diff --git a/.changeset/assets-script-version-analytics.md b/.changeset/assets-script-version-analytics.md new file mode 100644 index 0000000000..34777aff3e --- /dev/null +++ b/.changeset/assets-script-version-analytics.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/workers-shared": minor +--- + +Emit customer-facing Asset Worker request analytics separately + +Asset Worker requests now write a separate customer-facing analytics event that includes the customer Worker version UUID supplied by the assets pipeline. This enables analytics consumers to filter static asset metrics by the versions selected in the Workers dashboard without exposing operational Asset Worker fields. diff --git a/packages/workers-shared/asset-worker/src/configuration.ts b/packages/workers-shared/asset-worker/src/configuration.ts index c39102fdf7..de6cb33a4c 100644 --- a/packages/workers-shared/asset-worker/src/configuration.ts +++ b/packages/workers-shared/asset-worker/src/configuration.ts @@ -23,6 +23,7 @@ export const normalizeConfiguration = ( has_static_routing: configuration?.has_static_routing ?? false, account_id: configuration?.account_id ?? -1, script_id: configuration?.script_id ?? -1, + script_version_id: configuration?.script_version_id ?? "", debug: configuration?.debug ?? false, }; }; diff --git a/packages/workers-shared/asset-worker/src/customer-analytics.ts b/packages/workers-shared/asset-worker/src/customer-analytics.ts new file mode 100644 index 0000000000..b67a6e531b --- /dev/null +++ b/packages/workers-shared/asset-worker/src/customer-analytics.ts @@ -0,0 +1,60 @@ +import type { ReadyAnalytics } from "./types"; + +const VERSION = 1; + +type Data = { + accountId?: number; + scriptId?: number; + coloId?: number; + status?: number; + hostname?: string; + cacheStatus?: string; + scriptVersionId?: string; +}; + +/** + * Customer-facing request analytics. Keep this payload limited to fields that + * are intentionally exposed through the Workers Analytics GraphQL API. + */ +export class CustomerAnalytics { + private data: Data = {}; + + constructor(private readyAnalytics?: ReadyAnalytics) {} + + setData(newData: Partial) { + this.data = { ...this.data, ...newData }; + } + + write() { + if (!this.readyAnalytics) { + return; + } + + this.readyAnalytics.logEvent({ + version: VERSION, + accountId: this.data.accountId, + indexId: this.data.scriptId?.toString(), + doubles: [ + undefined, // double1 + this.data.coloId, // double2 + undefined, // double3 + undefined, // double4 + this.data.status, // double5 + ], + blobs: [ + this.data.hostname?.substring(0, 256), // blob1 + undefined, // blob2 + undefined, // blob3 + undefined, // blob4 + undefined, // blob5 + undefined, // blob6 + undefined, // blob7 + this.data.cacheStatus, // blob8 + undefined, // blob9 + undefined, // blob10 + undefined, // blob11 + this.data.scriptVersionId, // blob12 + ], + }); + } +} diff --git a/packages/workers-shared/asset-worker/src/worker.ts b/packages/workers-shared/asset-worker/src/worker.ts index 5250f09a84..5ede80ca6d 100644 --- a/packages/workers-shared/asset-worker/src/worker.ts +++ b/packages/workers-shared/asset-worker/src/worker.ts @@ -5,6 +5,7 @@ import { mockJaegerBinding } from "../../utils/tracing"; import { Analytics, EntrypointType, getRequestKind } from "./analytics"; import { AssetsManifest } from "./assets-manifest"; import { normalizeConfiguration } from "./configuration"; +import { CustomerAnalytics } from "./customer-analytics"; import { ExperimentAnalytics } from "./experiment-analytics"; import { canFetch, handleRequest } from "./handler"; import { handleError, submitMetrics } from "./utils/final-operations"; @@ -47,6 +48,7 @@ export type Env = { ENVIRONMENT: Environment; EXPERIMENT_ANALYTICS: ReadyAnalytics; ANALYTICS: ReadyAnalytics; + CUSTOMER_ANALYTICS: ReadyAnalytics; COLO_METADATA: ColoMetadata; UNSAFE_PERFORMANCE: UnsafePerformanceTimer; VERSION_METADATA: WorkerVersionMetadata; @@ -264,6 +266,7 @@ async function runFetchRequest( ): Promise { let sentry: ReturnType | undefined; const analytics = new Analytics(env.ANALYTICS); + const customerAnalytics = new CustomerAnalytics(env.CUSTOMER_ANALYTICS); const performance = new PerformanceTimer(env.UNSAFE_PERFORMANCE); const startTimeMs = performance.now(); @@ -312,6 +315,13 @@ async function runFetchRequest( cohort: cohort ?? "unknown", requestKind: getRequestKind(request), }); + customerAnalytics.setData({ + accountId: config.account_id, + scriptId: config.script_id, + coloId: env.COLO_METADATA.coloId, + hostname: url.hostname, + scriptVersionId: config.script_version_id || undefined, + }); } return await env.JAEGER.enterSpan("handleRequest", async (span) => { @@ -332,12 +342,18 @@ async function runFetchRequest( ); analytics.setData({ status: response.status }); + customerAnalytics.setData({ + status: response.status, + cacheStatus: analytics.getData("cacheStatus"), + }); return response; }); } catch (err) { + customerAnalytics.setData({ status: 500 }); return handleError(sentry, analytics, err); } finally { + customerAnalytics.write(); submitMetrics(analytics, performance, startTimeMs); } } diff --git a/packages/workers-shared/asset-worker/tests/customer-analytics.test.ts b/packages/workers-shared/asset-worker/tests/customer-analytics.test.ts new file mode 100644 index 0000000000..ebea6e5657 --- /dev/null +++ b/packages/workers-shared/asset-worker/tests/customer-analytics.test.ts @@ -0,0 +1,46 @@ +import { describe, it, vi } from "vitest"; +import { CustomerAnalytics } from "../src/customer-analytics"; +import type { ReadyAnalyticsEvent } from "../src/types"; + +describe("[Asset Worker] Customer Analytics", () => { + it("writes only the customer-facing request fields", ({ expect }) => { + let captured: ReadyAnalyticsEvent | undefined; + const analytics = new CustomerAnalytics({ + logEvent: vi.fn((event: ReadyAnalyticsEvent) => { + captured = event; + }), + }); + + analytics.setData({ + accountId: 123, + scriptId: 456, + coloId: 789, + status: 200, + hostname: "example.com", + cacheStatus: "HIT", + scriptVersionId: "01234567-89ab-cdef-0123-456789abcdef", + }); + analytics.write(); + + expect(captured).toEqual({ + version: 1, + accountId: 123, + indexId: "456", + doubles: [undefined, 789, undefined, undefined, 200], + blobs: [ + "example.com", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + "HIT", + undefined, + undefined, + undefined, + "01234567-89ab-cdef-0123-456789abcdef", + ], + }); + }); +}); diff --git a/packages/workers-shared/utils/types.ts b/packages/workers-shared/utils/types.ts index 1cd4374b3c..1cd2f0a84a 100644 --- a/packages/workers-shared/utils/types.ts +++ b/packages/workers-shared/utils/types.ts @@ -67,6 +67,7 @@ export const HeadersSchema = z .optional(); export const AssetConfigSchema = z.object({ + script_version_id: z.string().optional(), compatibility_date: z.string().optional(), compatibility_flags: z.array(z.string()).optional(), html_handling: z