import { getClient, type SanityInstance } from "@sanity/sdk"; import { type ConsentStatus, createBatchedStore, createSessionId, type TelemetryEvent, type TelemetryStore, } from "@sanity/telemetry"; import { assign, fromPromise, setup } from "xstate"; import type { OSBaseInput } from "./root.machine"; /** * @public */ export type WorkbenchUserProperties = { version: string; organizationId: string; environment: string; userAgent: string; }; /** * @internal */ export interface TelemetryInput extends OSBaseInput, WorkbenchUserProperties {} /** * TODO: this shouldn't be unique to the telemetry machine, * remove this and set it globally with a single client actor. */ const TELEMETRY_API_VERSION = "2024-11-12"; /** * 30 seconds, in milliseconds, is the default flush interval * for the telemetry store. */ const FLUSH_INTERVAL_MS = 30_000; type ConsentResult = { status: ConsentStatus }; const checkConsentLogic = fromPromise< ConsentResult, { instance: SanityInstance } >(async ({ input, signal }) => { try { const client = getClient(input.instance, { apiVersion: TELEMETRY_API_VERSION, }); return await client.request({ uri: "/intake/telemetry-status", tag: "telemetry-consent", signal, }); } catch { return { status: "undetermined" } satisfies ConsentResult; } }); type TelemetryContext = { instance: SanityInstance; store: TelemetryStore | null; userProperties: WorkbenchUserProperties; }; type TelemetryMachineEvent = | { type: "telemetry.start" } | { type: "telemetry.stop" }; export const telemetryLogic = setup({ types: { input: {} as TelemetryInput, context: {} as TelemetryContext, events: {} as TelemetryMachineEvent, }, actors: { checkConsent: checkConsentLogic, }, guards: { isConsentGranted: (_, params: { status: ConsentStatus }) => params.status === "granted", }, actions: { createStore: assign({ store: ({ context }) => { const sessionId = createSessionId(); const client = getClient(context.instance, { apiVersion: TELEMETRY_API_VERSION, }); const store = createBatchedStore(sessionId, { flushInterval: FLUSH_INTERVAL_MS, resolveConsent: () => client.request<{ status: ConsentStatus }>({ uri: "/intake/telemetry-status", tag: "telemetry-consent", }), sendEvents: (batch: TelemetryEvent[]) => client.request({ uri: "/intake/batch", method: "POST", body: { batch }, tag: "telemetry.batch", }), sendBeacon: (batch: TelemetryEvent[]) => { if (typeof navigator === "undefined") { return false; } return navigator.sendBeacon( client.getUrl("/intake/batch"), JSON.stringify({ batch }), ); }, }); store.logger.updateUserProperties(context.userProperties); return store; }, }), teardownStore: ({ context }) => { context.store?.end(); }, }, }).createMachine({ id: "telemetry", initial: "idle", context: ({ input }) => ({ instance: input.instance, store: null, userProperties: { version: input.version, organizationId: input.organizationId, environment: input.environment, userAgent: input.userAgent, }, }), states: { idle: { on: { "telemetry.start": { target: "checkingConsent", }, }, }, checkingConsent: { invoke: { src: "checkConsent", input: ({ context }) => ({ instance: context.instance, }), onDone: [ { guard: { type: "isConsentGranted", params: ({ event }) => ({ status: event.output.status, }), }, actions: [{ type: "createStore" }], target: "active", }, { target: "denied", }, ], }, }, active: { tags: ["telemetry-resolved"], exit: [{ type: "teardownStore" }], on: { "telemetry.stop": { target: "stopped" }, }, }, stopped: { type: "final", }, denied: { tags: ["telemetry-resolved"], }, }, });