Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
60 changes: 58 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import type {
} from "@telemetrydeck/sdk";
import { inject } from "vue";

export type TelemetryDeckMethod = "signal" | "queue" | "flush";
export type {
TelemetryDeckOptions,
TelemetryDeckPayload,
} from "@telemetrydeck/sdk";

export type TelemetryDeckMethod = keyof Pick<
TelemetryDeck,
"signal" | "queue" | "flush"
>;

export interface TelemetryDeckErrorMeta {
method: TelemetryDeckMethod;
Expand All @@ -19,7 +27,55 @@ export type TelemetryDeckErrorHandler = (
meta: TelemetryDeckErrorMeta,
) => void | Promise<void>;

export function useTelemetryDeck() {
export type TelemetryDeckSetClientUser = (clientUser: string) => Promise<void>;

type TelemetryDeckMethodReturn<TMethod extends "signal" | "queue" | "flush"> =
Awaited<ReturnType<TelemetryDeck[TMethod]>>;

type TelemetryDeckMethodParameters<
TMethod extends "signal" | "queue" | "flush",
> = Parameters<TelemetryDeck[TMethod]>;

export type TelemetryDeckSignal = (
...args: TelemetryDeckMethodParameters<"signal">
) => Promise<TelemetryDeckMethodReturn<"signal"> | undefined>;

export type TelemetryDeckQueue = (
...args: TelemetryDeckMethodParameters<"queue">
) => Promise<TelemetryDeckMethodReturn<"queue"> | undefined>;

export type TelemetryDeckFlush = (
...args: TelemetryDeckMethodParameters<"flush">
) => Promise<TelemetryDeckMethodReturn<"flush"> | undefined>;

export type TelemetryDeckGetQueueCount = () => number;

export type TelemetryDeckSafeSignal = (
type: string,
payload?: TelemetryDeckPayload,
options?: TelemetryDeckOptions,
) => Promise<void>;

export type TelemetryDeckSafeQueue = (
type: string,
payload?: TelemetryDeckPayload,
options?: TelemetryDeckOptions,
) => Promise<void>;

export type TelemetryDeckSafeFlush = () => Promise<void>;

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<TelemetryDeck>("td");
const onError = inject<TelemetryDeckErrorHandler | undefined>(
"tdOnError",
Expand Down
10 changes: 5 additions & 5 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
240 changes: 125 additions & 115 deletions src/tests/App.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading
Loading