From b689cd03d2836fb2e30fbe0ed0251c475b096444 Mon Sep 17 00:00:00 2001 From: Sally Mclean Date: Tue, 23 Jun 2026 10:30:23 +0200 Subject: [PATCH 1/4] types --- README.md | 17 ++++++++++++++ src/hooks/index.ts | 50 +++++++++++++++++++++++++++++++++++++++- src/tests/hooks.test.ts | 12 +++++----- src/tests/plugin.test.ts | 4 ++-- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 08b1b43..412f484 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,23 @@ await safeFlush(); `signal()`, `queue()`, and `flush()` return promises and may reject (for example on network failures). Use `safeSignal()`, `safeQueue()`, and `safeFlush()` for fire-and-forget analytics calls to avoid unhandled promise rejections. +## TypeScript Helpers + +The package exports named public types for the composable return value and each telemetry method. Prefer these named types when your application stores or accepts one of the functions outside of the component that calls `useTelemetryDeck()`. + +```ts +import type { + TelemetryDeckHooks, + TelemetryDeckSafeSignal, +} from "@peerigon/telemetrydeck-vue"; + +let safeSignal: TelemetryDeckSafeSignal | undefined; + +function installTelemetry(hooks: TelemetryDeckHooks) { + safeSignal = hooks.safeSignal; +} +``` + ## Contributions ### Local Development diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 57c70a1..65973fd 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -5,6 +5,11 @@ import type { } from "@telemetrydeck/sdk"; import { inject } from "vue"; +export type { + TelemetryDeckOptions, + TelemetryDeckPayload, +} from "@telemetrydeck/sdk"; + export type TelemetryDeckMethod = "signal" | "queue" | "flush"; export interface TelemetryDeckErrorMeta { @@ -19,7 +24,50 @@ export type TelemetryDeckErrorHandler = ( meta: TelemetryDeckErrorMeta, ) => void | Promise; -export function useTelemetryDeck() { +export type TelemetryDeckSetClientUser = (clientUser: string) => Promise; + +export type TelemetryDeckSignal = ( + type: string, + payload?: TelemetryDeckPayload, + options?: TelemetryDeckOptions, +) => Promise; + +export type TelemetryDeckQueue = ( + type: string, + payload?: TelemetryDeckPayload, + options?: TelemetryDeckOptions, +) => Promise; + +export type TelemetryDeckFlush = () => Promise; + +export type TelemetryDeckGetQueueCount = () => number; + +export type TelemetryDeckSafeSignal = ( + type: string, + payload?: TelemetryDeckPayload, + options?: TelemetryDeckOptions, +) => Promise; + +export type TelemetryDeckSafeQueue = ( + type: string, + payload?: TelemetryDeckPayload, + options?: TelemetryDeckOptions, +) => Promise; + +export type TelemetryDeckSafeFlush = () => Promise; + +export interface TelemetryDeckHooks { + setClientUser: TelemetryDeckSetClientUser; + signal: TelemetryDeckSignal; + queue: TelemetryDeckQueue; + flush: TelemetryDeckFlush; + getQueueCount: TelemetryDeckGetQueueCount; + safeSignal: TelemetryDeckSafeSignal; + safeQueue: TelemetryDeckSafeQueue; + safeFlush: TelemetryDeckSafeFlush; +} + +export function useTelemetryDeck(): TelemetryDeckHooks { const td = inject("td"); const onError = inject( "tdOnError", diff --git a/src/tests/hooks.test.ts b/src/tests/hooks.test.ts index 839b942..e84e7f1 100644 --- a/src/tests/hooks.test.ts +++ b/src/tests/hooks.test.ts @@ -1,7 +1,7 @@ import { mount } from "@vue/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { defineComponent } from "vue"; -import { useTelemetryDeck } from "../hooks"; +import { useTelemetryDeck, type TelemetryDeckHooks } from "../hooks"; const mockTelemetryDeck = { clientUser: "test-user", @@ -51,7 +51,7 @@ describe("useTelemetryDeck safe methods", () => { }, }, }); - const vm = wrapper.vm as unknown as ReturnType; + const vm = wrapper.vm as unknown as TelemetryDeckHooks; await expect( vm.safeSignal("ui.opened", signalPayload), @@ -98,7 +98,7 @@ describe("useTelemetryDeck safe methods", () => { }, }, }); - const vm = wrapper.vm as unknown as ReturnType; + const vm = wrapper.vm as unknown as TelemetryDeckHooks; await expect(vm.safeSignal("ui.opened")).resolves.toBeUndefined(); await expect(vm.safeQueue("button.clicked")).resolves.toBeUndefined(); @@ -141,7 +141,7 @@ describe("useTelemetryDeck safe methods", () => { }, }, }); - const vm = wrapper.vm as unknown as ReturnType; + const vm = wrapper.vm as unknown as TelemetryDeckHooks; await expect(vm.safeSignal("ui.opened")).resolves.toBeUndefined(); await expect(vm.safeQueue("button.clicked")).resolves.toBeUndefined(); @@ -180,7 +180,7 @@ describe("useTelemetryDeck safe methods", () => { }, }, }); - const vm = wrapper.vm as unknown as ReturnType; + const vm = wrapper.vm as unknown as TelemetryDeckHooks; await expect(vm.signal("ui.opened")).rejects.toBe(signalError); await expect(vm.queue("button.clicked")).rejects.toBe(queueError); @@ -197,7 +197,7 @@ describe("useTelemetryDeck safe methods", () => { }, }, }); - const vm = wrapper.vm as unknown as ReturnType; + const vm = wrapper.vm as unknown as TelemetryDeckHooks; expect(vm.getQueueCount()).toBe(2); }); diff --git a/src/tests/plugin.test.ts b/src/tests/plugin.test.ts index f420161..369a059 100644 --- a/src/tests/plugin.test.ts +++ b/src/tests/plugin.test.ts @@ -1,7 +1,7 @@ import { mount } from "@vue/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { defineComponent } from "vue"; -import { useTelemetryDeck } from "../hooks"; +import { useTelemetryDeck, type TelemetryDeckHooks } from "../hooks"; import { plugin } from "../plugin"; const { mockTelemetryDeck, telemetryDeckCtor } = vi.hoisted(() => { @@ -55,7 +55,7 @@ describe("plugin onError integration", () => { plugins: [[plugin, { appID: "test-app-id", onError }]], }, }); - const vm = wrapper.vm as unknown as ReturnType; + const vm = wrapper.vm as unknown as TelemetryDeckHooks; await expect(vm.safeSignal("ui.opened")).resolves.toBeUndefined(); await expect(vm.safeQueue("button.clicked")).resolves.toBeUndefined(); From 894ce2705fd2a44288d800e79892050068d14a93 Mon Sep 17 00:00:00 2001 From: Sally Mclean Date: Tue, 23 Jun 2026 10:35:26 +0200 Subject: [PATCH 2/4] smoketest --- package.json | 2 +- tests/exports/tsconfig.json | 7 +++++++ tests/exports/types.ts | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/exports/tsconfig.json create mode 100644 tests/exports/types.ts diff --git a/package.json b/package.json index 3203c71..877e9be 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "release": "semantic-release", "size": "size-limit", "test": "vitest --coverage", - "test:exports": "node tests/exports/import.mjs && node tests/exports/require.cjs", + "test:exports": "node tests/exports/import.mjs && node tests/exports/require.cjs && tsc --noEmit -p tests/exports/tsconfig.json", "test:pack": "node tests/pack/contents.mjs", "typecheck": "vue-tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.build.json" }, diff --git a/tests/exports/tsconfig.json b/tests/exports/tsconfig.json new file mode 100644 index 0000000..31f0d2e --- /dev/null +++ b/tests/exports/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.app.json", + "compilerOptions": { + "tsBuildInfoFile": "../../node_modules/.tmp/tsconfig.exports.tsbuildinfo" + }, + "include": ["types.ts"] +} diff --git a/tests/exports/types.ts b/tests/exports/types.ts new file mode 100644 index 0000000..c7f4de5 --- /dev/null +++ b/tests/exports/types.ts @@ -0,0 +1,21 @@ +import { + useTelemetryDeck, + type TelemetryDeckHooks, + type TelemetryDeckOptions, + type TelemetryDeckPayload, + type TelemetryDeckSafeSignal, + type TelemetryDeckSignal, +} from "@peerigon/telemetrydeck-vue"; + +const hooks: TelemetryDeckHooks = useTelemetryDeck(); +const signal: TelemetryDeckSignal = hooks.signal; +const safeSignal: TelemetryDeckSafeSignal = hooks.safeSignal; + +const payload: TelemetryDeckPayload = { source: "type-smoke-test" }; +const options: TelemetryDeckOptions = { + appID: "test-app-id", + clientUser: "test-user", +}; + +void signal("example.signal", payload, options); +void safeSignal("example.safeSignal", payload, options); From cda0105ea0315614579255cb169a20048af32964 Mon Sep 17 00:00:00 2001 From: Sally Mclean Date: Tue, 23 Jun 2026 10:45:39 +0200 Subject: [PATCH 3/4] types from sdk --- src/hooks/index.ts | 28 ++++++++++++++++++---------- src/plugin/index.ts | 10 +++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 65973fd..d585bc3 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -10,7 +10,10 @@ export type { TelemetryDeckPayload, } from "@telemetrydeck/sdk"; -export type TelemetryDeckMethod = "signal" | "queue" | "flush"; +export type TelemetryDeckMethod = keyof Pick< + TelemetryDeck, + "signal" | "queue" | "flush" +>; export interface TelemetryDeckErrorMeta { method: TelemetryDeckMethod; @@ -26,19 +29,24 @@ export type TelemetryDeckErrorHandler = ( export type TelemetryDeckSetClientUser = (clientUser: string) => Promise; +type TelemetryDeckMethodReturn = + Awaited>; + +type TelemetryDeckMethodParameters< + TMethod extends "signal" | "queue" | "flush", +> = Parameters; + export type TelemetryDeckSignal = ( - type: string, - payload?: TelemetryDeckPayload, - options?: TelemetryDeckOptions, -) => Promise; + ...args: TelemetryDeckMethodParameters<"signal"> +) => Promise | undefined>; export type TelemetryDeckQueue = ( - type: string, - payload?: TelemetryDeckPayload, - options?: TelemetryDeckOptions, -) => Promise; + ...args: TelemetryDeckMethodParameters<"queue"> +) => Promise | undefined>; -export type TelemetryDeckFlush = () => Promise; +export type TelemetryDeckFlush = ( + ...args: TelemetryDeckMethodParameters<"flush"> +) => Promise | undefined>; export type TelemetryDeckGetQueueCount = () => number; diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 6404d61..e8f1a79 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -1,14 +1,14 @@ -import TelemetryDeck from "@telemetrydeck/sdk"; +import TelemetryDeck, { type TelemetryDeckOptions } from "@telemetrydeck/sdk"; import type { TelemetryDeckErrorHandler } from "../hooks"; interface TelemetryDeckApp { provide: (key: string, value: unknown) => void; } -export interface TelemetryDeckPluginOptions { - appID: string; - clientUser?: string; - testMode?: boolean; +export interface TelemetryDeckPluginOptions extends Pick< + TelemetryDeckOptions, + "appID" | "clientUser" | "testMode" +> { onError?: TelemetryDeckErrorHandler; } From 0f108590d40195c42ae7b440f054b4cde32eb6a7 Mon Sep 17 00:00:00 2001 From: Sally Mclean Date: Tue, 23 Jun 2026 10:48:10 +0200 Subject: [PATCH 4/4] fix --- src/tests/App.test.ts | 240 ++++++++++++++++++++++-------------------- 1 file changed, 125 insertions(+), 115 deletions(-) diff --git a/src/tests/App.test.ts b/src/tests/App.test.ts index 13351ea..4402f0c 100644 --- a/src/tests/App.test.ts +++ b/src/tests/App.test.ts @@ -90,121 +90,131 @@ describe("App", () => { it("calls TelemetryDeck methods from the demo controls", async () => { const wrapper = mountApp(); - - mockTelemetryDeck.signal.mockResolvedValueOnce({ accepted: true }); - - await wrapper.find("#btnSignalClick").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.signal).toHaveBeenCalledWith( - "example_signal_event_name", - expect.objectContaining({ - custom_data: "other_data", - source: "signal", - timestamp: expect.any(String), - }), - undefined, - ); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - 'signal response: {"accepted":true}', - ); - - await wrapper.find("#btnQueueClick").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.queue).toHaveBeenCalledWith( - "example_queue_event_name", - expect.objectContaining({ - custom_data: "other_data", - source: "queue", - timestamp: expect.any(String), - }), - undefined, - ); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - "queue promise resolved without response", - ); - expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("1"); - - await wrapper.find("#btnQueueClickWithOptions").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.queue).toHaveBeenCalledWith( - "example_queue_event_name_with_options", - expect.objectContaining({ - custom_data: "other_data", - source: "queue_with_options", - timestamp: expect.any(String), - }), - { testMode: true, clientUser: "other_user", appID: "other_app_id" }, - ); - - await wrapper.find("#btnSignalClickWithOptions").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.signal).toHaveBeenCalledWith( - "example_signal_event_name_with_options", - expect.objectContaining({ - custom_data: "other_data", - source: "signal_with_options", - timestamp: expect.any(String), - }), - { testMode: true, clientUser: "other_user", appID: "other_app_id" }, - ); - - await wrapper.find("#btnFlushClick").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.flush).toHaveBeenCalledTimes(1); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - "flush promise resolved without response", - ); - expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("0"); - - await wrapper.find("#btnSafeSignalClick").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.signal).toHaveBeenCalledWith( - "example_safe_signal_event_name", - expect.objectContaining({ - source: "safe_signal", - }), - undefined, - ); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - "safeSignal promise resolved", - ); - - await wrapper.find("#btnSafeQueueClick").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.queue).toHaveBeenCalledWith( - "example_safe_queue_event_name", - expect.objectContaining({ - source: "safe_queue", - }), - undefined, - ); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - "safeQueue promise resolved", - ); - expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("1"); - - await wrapper.find("#btnSafeFlushClick").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.flush).toHaveBeenCalledTimes(2); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - "safeFlush promise resolved", - ); - expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("0"); - - let previousClientUser = mockTelemetryDeck.clientUser; - await wrapper.find("#btnSetClient").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.clientUser).not.toBe(""); - expect(mockTelemetryDeck.clientUser).not.toBe(previousClientUser); - - previousClientUser = mockTelemetryDeck.clientUser; - await wrapper.find("#btnSetClient").trigger("click"); - await waitForClickHandler(); - expect(mockTelemetryDeck.clientUser).not.toBe(""); - expect(mockTelemetryDeck.clientUser).not.toBe(previousClientUser); - expect(wrapper.find("#lastTelemetryResult").text()).toBe( - "setClientUser completed", - ); + const randomSpy = vi + .spyOn(Math, "random") + .mockReturnValueOnce(0.694) + .mockReturnValueOnce(0.695); + + try { + mockTelemetryDeck.signal.mockResolvedValueOnce({ accepted: true }); + + await wrapper.find("#btnSignalClick").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.signal).toHaveBeenCalledWith( + "example_signal_event_name", + expect.objectContaining({ + custom_data: "other_data", + source: "signal", + timestamp: expect.any(String), + }), + undefined, + ); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + 'signal response: {"accepted":true}', + ); + + await wrapper.find("#btnQueueClick").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.queue).toHaveBeenCalledWith( + "example_queue_event_name", + expect.objectContaining({ + custom_data: "other_data", + source: "queue", + timestamp: expect.any(String), + }), + undefined, + ); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + "queue promise resolved without response", + ); + expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("1"); + + await wrapper.find("#btnQueueClickWithOptions").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.queue).toHaveBeenCalledWith( + "example_queue_event_name_with_options", + expect.objectContaining({ + custom_data: "other_data", + source: "queue_with_options", + timestamp: expect.any(String), + }), + { testMode: true, clientUser: "other_user", appID: "other_app_id" }, + ); + + await wrapper.find("#btnSignalClickWithOptions").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.signal).toHaveBeenCalledWith( + "example_signal_event_name_with_options", + expect.objectContaining({ + custom_data: "other_data", + source: "signal_with_options", + timestamp: expect.any(String), + }), + { testMode: true, clientUser: "other_user", appID: "other_app_id" }, + ); + + await wrapper.find("#btnFlushClick").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.flush).toHaveBeenCalledTimes(1); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + "flush promise resolved without response", + ); + expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("0"); + + await wrapper.find("#btnSafeSignalClick").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.signal).toHaveBeenCalledWith( + "example_safe_signal_event_name", + expect.objectContaining({ + source: "safe_signal", + }), + undefined, + ); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + "safeSignal promise resolved", + ); + + await wrapper.find("#btnSafeQueueClick").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.queue).toHaveBeenCalledWith( + "example_safe_queue_event_name", + expect.objectContaining({ + source: "safe_queue", + }), + undefined, + ); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + "safeQueue promise resolved", + ); + expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("1"); + + await wrapper.find("#btnSafeFlushClick").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.flush).toHaveBeenCalledTimes(2); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + "safeFlush promise resolved", + ); + expect(wrapper.find("#queuedTelemetryEvents").text()).toBe("0"); + + let previousClientUser = mockTelemetryDeck.clientUser; + await wrapper.find("#btnSetClient").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.clientUser).toBe("user694"); + expect(mockTelemetryDeck.clientUser).not.toBe(""); + expect(mockTelemetryDeck.clientUser).not.toBe(previousClientUser); + + previousClientUser = mockTelemetryDeck.clientUser; + await wrapper.find("#btnSetClient").trigger("click"); + await waitForClickHandler(); + expect(mockTelemetryDeck.clientUser).toBe("user695"); + expect(mockTelemetryDeck.clientUser).not.toBe(""); + expect(mockTelemetryDeck.clientUser).not.toBe(previousClientUser); + expect(wrapper.find("#lastTelemetryResult").text()).toBe( + "setClientUser completed", + ); + } finally { + randomSpy.mockRestore(); + } }); it("displays TelemetryDeck errors captured by the demo onError handler", async () => {