import * as z from "zod/mini"; import { appEventIdentityTypeSchema } from "./identity-type.js"; import { jsonObjectWithoutPrototypeKeySchema, type ProtocolJsonValue, protocolJsonObjectSchema, protocolJsonValueSchema, } from "./json-value.js"; export { appEventIdentityTypeSchema } from "./identity-type.js"; export const appEventIdentitySchema = z.object({ type: appEventIdentityTypeSchema, value: z.string().check(z.trim(), z.minLength(1)), }); type AppEventFlagValue = | string | number | boolean | null | AppEventFlagValue[] | { [key: string]: AppEventFlagValue }; export type AppEventJsonValue = ProtocolJsonValue; const appEventFlagValueSchema: z.ZodMiniType = z.lazy(() => z.union([ z.string(), z.number(), z.boolean(), z.null(), z.array(appEventFlagValueSchema), z.record(z.string(), appEventFlagValueSchema), ]), ); export const appEventFlagProviderSchema = z.object({ name: z.string().check(z.trim(), z.minLength(1)), project: z.optional(z.string().check(z.trim(), z.minLength(1))), environment: z.optional(z.string().check(z.trim(), z.minLength(1))), }); export const appEventFlagSchema = z.object({ key: z.string().check(z.trim(), z.minLength(1)), target: z.optional(z.string().check(z.trim(), z.minLength(1))), value: appEventFlagValueSchema, variant: z.optional(z.string().check(z.trim(), z.minLength(1))), origin: z.optional( z.enum(["runtime_config", "trigger_context", "provider_sync", "api"]), ), provider: z.optional(appEventFlagProviderSchema), status: z.optional(z.string().check(z.trim(), z.minLength(1))), reason: z.optional(z.string().check(z.trim(), z.minLength(1))), evaluatedAt: z.optional(z.string().check(z.trim(), z.minLength(1))), }); export const appEventCapabilitySchema = z.object({ key: z.string().check(z.trim(), z.minLength(1)), source: z.optional(z.string().check(z.trim(), z.minLength(1))), provider: z.optional(appEventFlagProviderSchema), }); const appEventPageSchema = z.object({ path: z.optional(z.string()), referrer: z.optional(z.string()), search: z.optional(z.string()), }); const externalIdTypeSchema = z.string().check( z.trim(), z.minLength(1), z.regex(/^[a-z0-9][a-z0-9_.-]*$/, { message: "External ID type must start with a lowercase letter or number and may contain lowercase letters, numbers, underscores, dots, or hyphens.", }), ); export const appEventExternalIdSchema = z.object({ id: z.pipe( z.string().check(z.trim(), z.minLength(1)), z.transform((value) => value.trim()), ), type: externalIdTypeSchema, collection: z.literal("users"), encoding: z.literal("none"), }); export type AppEventExternalId = z.output; export const appEventContextSchema = z.object({ page: z.optional(appEventPageSchema), locale: z.optional(z.string()), userAgent: z.optional(z.string()), externalIds: z.optional( z.array(appEventExternalIdSchema).check(z.maxLength(20)), ), flags: z.optional(z.array(appEventFlagSchema)), capabilities: z.optional(z.array(appEventCapabilitySchema)), }); // See docs/specs/2026-08-28-sdk-customer-occurrence.md. export const appEventOccurrenceTypeSchema = z.enum(["track", "page", "screen"]); export type AppEventOccurrenceType = z.output< typeof appEventOccurrenceTypeSchema >; export const appEventBaseSchema = z.object({ traits: z.optional(z.record(z.string(), z.unknown())), identities: z.optional(z.array(appEventIdentitySchema)), context: z.optional(appEventContextSchema), messageId: z.optional(z.string()), timestamp: z.optional(z.number()), }); const appEventSurveyReferenceSchema = z.object({ surveyId: z.string(), }); export const reservedSystemAppEventNames = [ "Response Submitted", "Flow Completed", "Flow Viewed", "Flow Dismissed", "Flow Action Succeeded", "Identified", ] as const; const reservedSystemAppEventNameLowercaseSet = new Set( reservedSystemAppEventNames.map((event) => event.toLowerCase()), ); const appEventSystemTrackNameSchema = z.enum(["Flow Viewed", "Flow Dismissed"]); export const appEventCustomerNameSchema = z.string().check( z.trim(), z.minLength(1), z.maxLength(255), z.check((ctx) => { if (!reservedSystemAppEventNameLowercaseSet.has(ctx.value.toLowerCase())) { return; } ctx.issues.push({ code: "custom", input: ctx.value, message: "Customer app event names cannot use reserved system event names.", continue: false, }); }), ); export const appEventSystemBaseSchema = z.extend(appEventBaseSchema, { origin: z.literal("system"), type: z.optional(z.literal("track")), }); // Governed by docs/specs/2026-08-09-gx-flow-analytics-event-contract.md. export const FLOW_ACTION_SUCCEEDED_EVENT_NAME = "Flow Action Succeeded"; export const FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID = "gx_flow_action_succeeded"; export const FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION = 1; const normalizedRequiredStringSchema = z.pipe( z.string().check(z.trim(), z.minLength(1)), z.transform((value) => value.trim()), ); const positiveSafeIntegerSchema = z .number() .check(z.int(), z.positive(), z.maximum(Number.MAX_SAFE_INTEGER)); export const flowEventSurfaceSchema = z.enum(["widget", "web_page"]); export type FlowEventSurface = z.output; export const flowActionKindSchema = z.enum([ "runtime-action", "navigation-action", "remote-action", ]); const flowActionSucceededV1PropertyShape = { gx_event_schema: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID), gx_event_schema_version: z.literal( FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION, ), gx_flow_id: normalizedRequiredStringSchema, gx_flow_version_id: normalizedRequiredStringSchema, gx_flow_version_number: positiveSafeIntegerSchema, gx_flow_run_id: normalizedRequiredStringSchema, gx_flow_surface: flowEventSurfaceSchema, gx_action_id: normalizedRequiredStringSchema, gx_action_invocation_id: normalizedRequiredStringSchema, gx_action_key: normalizedRequiredStringSchema, gx_action_version: positiveSafeIntegerSchema, gx_action_kind: flowActionKindSchema, }; const flowActionSucceededV1KnownPropertyKeys = new Set( Object.keys(flowActionSucceededV1PropertyShape), ); const FLOW_COMPLETION_ID_PROPERTY_KEY = "gx_flow_completion_id"; const FLOW_OCCURRENCE_ID_PROPERTY_KEY = "gx_flow_occurrence_id"; export const flowActionSucceededV1PropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall( z.object(flowActionSucceededV1PropertyShape), protocolJsonValueSchema, ) .check( z.check((ctx) => { for (const key of Object.keys(ctx.value)) { if ( flowActionSucceededV1KnownPropertyKeys.has(key) || (key.startsWith("gx_") && key !== "gx_flow_ending_id" && key !== "gx_response_id" && key !== FLOW_COMPLETION_ID_PROPERTY_KEY) ) { continue; } ctx.issues.push({ code: "custom", input: ctx.value, message: `Unsupported Flow Action Succeeded property: ${key}`, path: [key], continue: false, }); } }), ), ); export const flowActionSucceededV1ContractSchema = z.strictObject({ event: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_NAME), eventSchema: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION), properties: flowActionSucceededV1PropertiesSchema, }); const maximumJavascriptDateTimestamp = 8_640_000_000_000_000; const flowActionSucceededTimestampSchema = z .number() .check(z.int(), z.nonnegative(), z.maximum(maximumJavascriptDateTimestamp)); export const flowActionSucceededMessageIdSchema = z .string() .check(z.trim(), z.minLength(1), z.maxLength(255)); export const flowActionSucceededV1MessageIdPrefix = "gx_flow_action_succeeded:v1:"; export const appEventFlowActionSucceededV1Schema = z .extend(appEventSystemBaseSchema, { event: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_NAME), eventSchema: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION), messageId: flowActionSucceededMessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowActionSucceededV1PropertiesSchema, }) .check( z.check((context) => { const expectedMessageId = `${flowActionSucceededV1MessageIdPrefix}${context.value.properties.gx_action_invocation_id}`; if (context.value.messageId === expectedMessageId) { return; } context.issues.push({ code: "custom", input: context.value, message: "Flow Action Succeeded message identity must match its action invocation.", path: ["messageId"], continue: false, }); }), ); export const flowActionSucceededUnknownVersionPropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall( z.object({ gx_event_schema: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID), gx_event_schema_version: positiveSafeIntegerSchema, }), protocolJsonValueSchema, ) .check( z.check((ctx) => { if (!(FLOW_COMPLETION_ID_PROPERTY_KEY in ctx.value)) { return; } ctx.issues.push({ code: "custom", input: ctx.value, message: `${FLOW_COMPLETION_ID_PROPERTY_KEY} is server-owned.`, path: [FLOW_COMPLETION_ID_PROPERTY_KEY], continue: false, }); }), ), ); export const appEventFlowActionSucceededUnknownVersionSchema = z .extend(appEventSystemBaseSchema, { event: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_NAME), eventSchema: z.literal(FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID), eventSchemaVersion: positiveSafeIntegerSchema.check( z.refine( (value) => value !== FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION, { message: "Expected a non-v1 Flow Action Succeeded schema version." }, ), ), messageId: flowActionSucceededMessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowActionSucceededUnknownVersionPropertiesSchema, }) .check( z.check((context) => { if ( context.value.properties.gx_event_schema_version === context.value.eventSchemaVersion ) { return; } context.issues.push({ code: "custom", input: context.value, message: "Flow Action Succeeded property schema version must match its envelope.", path: ["properties", "gx_event_schema_version"], continue: false, }); }), ); export type FlowActionSucceededUnknownVersionAppEvent = z.output< typeof appEventFlowActionSucceededUnknownVersionSchema >; export type FlowActionSucceededV1AppEvent = z.output< typeof appEventFlowActionSucceededV1Schema >; export const buildFlowActionSucceededV1AppEvent = (input: { event: FlowActionSucceededV1Contract; timestamp: number; }): FlowActionSucceededV1AppEvent => appEventFlowActionSucceededV1Schema.parse({ origin: "system", ...input.event, messageId: `${flowActionSucceededV1MessageIdPrefix}${input.event.properties.gx_action_invocation_id}`, timestamp: input.timestamp, }); export const flowActionSucceededV1InputSchema = z.strictObject({ flowId: normalizedRequiredStringSchema, flowVersionId: normalizedRequiredStringSchema, flowVersionNumber: positiveSafeIntegerSchema, flowRunId: normalizedRequiredStringSchema, flowSurface: flowEventSurfaceSchema, actionPlacementId: normalizedRequiredStringSchema, actionInvocationId: normalizedRequiredStringSchema, actionKey: normalizedRequiredStringSchema, actionVersion: positiveSafeIntegerSchema, actionKind: flowActionKindSchema, }); export type FlowActionSucceededV1Input = z.input< typeof flowActionSucceededV1InputSchema >; export type FlowActionSucceededV1Contract = z.output< typeof flowActionSucceededV1ContractSchema >; export const buildFlowActionSucceededV1 = ( input: FlowActionSucceededV1Input, ): FlowActionSucceededV1Contract => { const parsed = flowActionSucceededV1InputSchema.parse(input); return flowActionSucceededV1ContractSchema.parse({ event: FLOW_ACTION_SUCCEEDED_EVENT_NAME, eventSchema: FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_ID, gx_event_schema_version: FLOW_ACTION_SUCCEEDED_EVENT_SCHEMA_VERSION, gx_flow_id: parsed.flowId, gx_flow_version_id: parsed.flowVersionId, gx_flow_version_number: parsed.flowVersionNumber, gx_flow_run_id: parsed.flowRunId, gx_flow_surface: parsed.flowSurface, gx_action_id: parsed.actionPlacementId, gx_action_invocation_id: parsed.actionInvocationId, gx_action_key: parsed.actionKey, gx_action_version: parsed.actionVersion, gx_action_kind: parsed.actionKind, }, }); }; export const FLOW_VIEWED_EVENT_NAME = "Flow Viewed"; export const FLOW_VIEWED_EVENT_SCHEMA_ID = "gx_flow_viewed"; export const FLOW_VIEWED_EVENT_SCHEMA_VERSION = 1; export const FLOW_COMPLETED_EVENT_NAME = "Flow Completed"; export const FLOW_COMPLETED_EVENT_SCHEMA_ID = "gx_flow_completed"; export const FLOW_COMPLETED_EVENT_SCHEMA_VERSION = 1; export const FLOW_DISMISSED_EVENT_NAME = "Flow Dismissed"; export const FLOW_DISMISSED_EVENT_SCHEMA_ID = "gx_flow_dismissed"; export const FLOW_DISMISSED_EVENT_SCHEMA_VERSION = 1; export const flowViewedV1MessageIdPrefix = "gx_flow_viewed:v1:"; export const flowCompletedV1MessageIdPrefix = "gx_flow_completed:v1:"; export const flowDismissedV1MessageIdPrefix = "gx_flow_dismissed:v1:"; const flowLifecycleV1CommonPropertyShape = { gx_flow_id: normalizedRequiredStringSchema, gx_flow_version_id: normalizedRequiredStringSchema, gx_flow_version_number: positiveSafeIntegerSchema, gx_flow_run_id: normalizedRequiredStringSchema, gx_flow_surface: flowEventSurfaceSchema, }; const flowViewedV1PropertyShape = { gx_event_schema: z.literal(FLOW_VIEWED_EVENT_SCHEMA_ID), gx_event_schema_version: z.literal(FLOW_VIEWED_EVENT_SCHEMA_VERSION), ...flowLifecycleV1CommonPropertyShape, }; const flowViewedV1KnownPropertyKeys = new Set( Object.keys(flowViewedV1PropertyShape), ); export const flowViewedV1PropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall(z.object(flowViewedV1PropertyShape), protocolJsonValueSchema) .check( z.check((ctx) => { for (const key of Object.keys(ctx.value)) { if ( flowViewedV1KnownPropertyKeys.has(key) || (key.startsWith("gx_") && key !== "gx_flow_ending_id" && key !== "gx_response_id" && key !== FLOW_COMPLETION_ID_PROPERTY_KEY && key !== FLOW_OCCURRENCE_ID_PROPERTY_KEY) ) { continue; } ctx.issues.push({ code: "custom", input: ctx.value, message: `Unsupported Flow Viewed property: ${key}`, path: [key], continue: false, }); } }), ), ); const flowViewedReservedPropertyKeys = new Set([ "gx_flow_ending_id", "gx_response_id", "response_signals", FLOW_COMPLETION_ID_PROPERTY_KEY, FLOW_OCCURRENCE_ID_PROPERTY_KEY, ]); export const flowViewedUnknownVersionPropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall( z.object({ gx_event_schema: z.literal(FLOW_VIEWED_EVENT_SCHEMA_ID), gx_event_schema_version: positiveSafeIntegerSchema, }), protocolJsonValueSchema, ) .check( z.check((ctx) => { for (const key of flowViewedReservedPropertyKeys) { if (!(key in ctx.value)) { continue; } ctx.issues.push({ code: "custom", input: ctx.value, message: `Unsupported Flow Viewed property: ${key}`, path: [key], continue: false, }); } }), ), ); const flowCompletedV1PropertyShape = { gx_event_schema: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_ID), gx_event_schema_version: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_VERSION), ...flowLifecycleV1CommonPropertyShape, gx_flow_completion_id: normalizedRequiredStringSchema, }; const flowCompletedV1KnownPropertyKeys = new Set( Object.keys(flowCompletedV1PropertyShape), ); export const flowCompletedV1PropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall(z.object(flowCompletedV1PropertyShape), protocolJsonValueSchema) .check( z.check((ctx) => { for (const key of Object.keys(ctx.value)) { if ( flowCompletedV1KnownPropertyKeys.has(key) || (key.startsWith("gx_") && key !== "gx_flow_ending_id" && key !== "gx_response_id" && key !== FLOW_OCCURRENCE_ID_PROPERTY_KEY) ) { continue; } ctx.issues.push({ code: "custom", input: ctx.value, message: `Unsupported Flow Completed property: ${key}`, path: [key], continue: false, }); } }), ), ); export const flowCompletedUnknownVersionPropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z.catchall( z.object({ gx_event_schema: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_ID), gx_event_schema_version: positiveSafeIntegerSchema, }), protocolJsonValueSchema, ), ); const flowDismissedV1PropertyShape = { gx_event_schema: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_ID), gx_event_schema_version: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_VERSION), ...flowLifecycleV1CommonPropertyShape, }; const flowDismissedV1KnownPropertyKeys = new Set( Object.keys(flowDismissedV1PropertyShape), ); export const flowDismissedV1PropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall(z.object(flowDismissedV1PropertyShape), protocolJsonValueSchema) .check( z.check((ctx) => { for (const key of Object.keys(ctx.value)) { if ( flowDismissedV1KnownPropertyKeys.has(key) || (key.startsWith("gx_") && key !== "gx_flow_ending_id" && key !== "gx_response_id" && key !== FLOW_COMPLETION_ID_PROPERTY_KEY && key !== FLOW_OCCURRENCE_ID_PROPERTY_KEY) ) { continue; } ctx.issues.push({ code: "custom", input: ctx.value, message: `Unsupported Flow Dismissed property: ${key}`, path: [key], continue: false, }); } }), ), ); /** * Future Flow Dismissed versions are retained as opaque GX events. They must * not acquire the current v1 lifecycle meaning merely because their schema ID * is known, and they cannot carry the server-owned Flow completion identity. */ export const flowDismissedUnknownVersionPropertiesSchema = z.pipe( jsonObjectWithoutPrototypeKeySchema, z .catchall( z.object({ gx_event_schema: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_ID), gx_event_schema_version: positiveSafeIntegerSchema, }), protocolJsonValueSchema, ) .check( z.check((ctx) => { if (!(FLOW_COMPLETION_ID_PROPERTY_KEY in ctx.value)) { return; } ctx.issues.push({ code: "custom", input: ctx.value, message: `${FLOW_COMPLETION_ID_PROPERTY_KEY} is server-owned.`, path: [FLOW_COMPLETION_ID_PROPERTY_KEY], continue: false, }); }), ), ); export const flowViewedV1ContractSchema = z.strictObject({ event: z.literal(FLOW_VIEWED_EVENT_NAME), eventSchema: z.literal(FLOW_VIEWED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_VIEWED_EVENT_SCHEMA_VERSION), properties: flowViewedV1PropertiesSchema, }); export const flowCompletedV1ContractSchema = z.strictObject({ event: z.literal(FLOW_COMPLETED_EVENT_NAME), eventSchema: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_VERSION), properties: flowCompletedV1PropertiesSchema, }); export const flowDismissedV1ContractSchema = z.strictObject({ event: z.literal(FLOW_DISMISSED_EVENT_NAME), eventSchema: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_VERSION), properties: flowDismissedV1PropertiesSchema, }); const flowLifecycleV1CommonInputShape = { flowId: normalizedRequiredStringSchema, flowVersionId: normalizedRequiredStringSchema, flowVersionNumber: positiveSafeIntegerSchema, flowRunId: normalizedRequiredStringSchema, flowSurface: flowEventSurfaceSchema, }; export const flowViewedV1InputSchema = z.strictObject({ ...flowLifecycleV1CommonInputShape, }); export const flowCompletedV1InputSchema = z.strictObject({ ...flowLifecycleV1CommonInputShape, flowCompletionId: normalizedRequiredStringSchema, }); export const flowDismissedV1InputSchema = z.strictObject({ ...flowLifecycleV1CommonInputShape, }); export type FlowViewedV1Input = z.input; export type FlowCompletedV1Input = z.input; export type FlowDismissedV1Input = z.input; export type FlowViewedV1Contract = z.output; export type FlowCompletedV1Contract = z.output< typeof flowCompletedV1ContractSchema >; export type FlowDismissedV1Contract = z.output< typeof flowDismissedV1ContractSchema >; const toFlowLifecycleV1CommonProperties = (input: { flowId: string; flowVersionId: string; flowVersionNumber: number; flowRunId: string; flowSurface: z.output; }) => ({ gx_flow_id: input.flowId, gx_flow_version_id: input.flowVersionId, gx_flow_version_number: input.flowVersionNumber, gx_flow_run_id: input.flowRunId, gx_flow_surface: input.flowSurface, }); export const buildFlowViewedV1 = ( input: FlowViewedV1Input, ): FlowViewedV1Contract => { const parsed = flowViewedV1InputSchema.parse(input); return flowViewedV1ContractSchema.parse({ event: FLOW_VIEWED_EVENT_NAME, eventSchema: FLOW_VIEWED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_VIEWED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: FLOW_VIEWED_EVENT_SCHEMA_ID, gx_event_schema_version: FLOW_VIEWED_EVENT_SCHEMA_VERSION, ...toFlowLifecycleV1CommonProperties(parsed), }, }); }; export const buildFlowCompletedV1 = ( input: FlowCompletedV1Input, ): FlowCompletedV1Contract => { const parsed = flowCompletedV1InputSchema.parse(input); return flowCompletedV1ContractSchema.parse({ event: FLOW_COMPLETED_EVENT_NAME, eventSchema: FLOW_COMPLETED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_COMPLETED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: FLOW_COMPLETED_EVENT_SCHEMA_ID, gx_event_schema_version: FLOW_COMPLETED_EVENT_SCHEMA_VERSION, ...toFlowLifecycleV1CommonProperties(parsed), gx_flow_completion_id: parsed.flowCompletionId, }, }); }; export const buildFlowDismissedV1 = ( input: FlowDismissedV1Input, ): FlowDismissedV1Contract => { const parsed = flowDismissedV1InputSchema.parse(input); return flowDismissedV1ContractSchema.parse({ event: FLOW_DISMISSED_EVENT_NAME, eventSchema: FLOW_DISMISSED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_DISMISSED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: FLOW_DISMISSED_EVENT_SCHEMA_ID, gx_event_schema_version: FLOW_DISMISSED_EVENT_SCHEMA_VERSION, ...toFlowLifecycleV1CommonProperties(parsed), }, }); }; const flowViewedV1MessageIdSchema = flowActionSucceededMessageIdSchema.check( z.refine( (value) => value.startsWith(flowViewedV1MessageIdPrefix) && value.length > flowViewedV1MessageIdPrefix.length, { message: "Flow Viewed message identity must contain its occurrence identity.", }, ), ); const flowDismissedV1MessageIdSchema = flowActionSucceededMessageIdSchema.check( z.refine( (value) => value.startsWith(flowDismissedV1MessageIdPrefix) && value.length > flowDismissedV1MessageIdPrefix.length, { message: "Flow Dismissed message identity must contain its occurrence identity.", }, ), ); export const appEventFlowViewedV1Schema = z.strictObject({ ...appEventSystemBaseSchema.shape, event: z.literal(FLOW_VIEWED_EVENT_NAME), eventSchema: z.literal(FLOW_VIEWED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_VIEWED_EVENT_SCHEMA_VERSION), messageId: flowViewedV1MessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowViewedV1PropertiesSchema, }); export const appEventFlowViewedUnknownVersionSchema = z .strictObject({ ...appEventSystemBaseSchema.shape, event: z.literal(FLOW_VIEWED_EVENT_NAME), eventSchema: z.literal(FLOW_VIEWED_EVENT_SCHEMA_ID), eventSchemaVersion: positiveSafeIntegerSchema.check( z.refine((value) => value !== FLOW_VIEWED_EVENT_SCHEMA_VERSION, { message: "Expected a non-v1 Flow Viewed schema version.", }), ), messageId: flowActionSucceededMessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowViewedUnknownVersionPropertiesSchema, }) .check( z.check((context) => { if ( context.value.properties.gx_event_schema_version === context.value.eventSchemaVersion ) { return; } context.issues.push({ code: "custom", input: context.value, message: "Flow Viewed property schema version must match its envelope.", path: ["properties", "gx_event_schema_version"], continue: false, }); }), ); export const appEventFlowCompletedV1Schema = z .strictObject({ ...appEventSystemBaseSchema.shape, event: z.literal(FLOW_COMPLETED_EVENT_NAME), eventSchema: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_VERSION), messageId: flowActionSucceededMessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowCompletedV1PropertiesSchema, }) .check( z.check((context) => { const expectedMessageId = `${flowCompletedV1MessageIdPrefix}${context.value.properties.gx_flow_completion_id}`; if (context.value.messageId === expectedMessageId) { return; } context.issues.push({ code: "custom", input: context.value, message: "Flow Completed message identity must match its completion identity.", path: ["messageId"], continue: false, }); }), ); export const appEventFlowCompletedUnknownVersionSchema = z .strictObject({ ...appEventSystemBaseSchema.shape, event: z.literal(FLOW_COMPLETED_EVENT_NAME), eventSchema: z.literal(FLOW_COMPLETED_EVENT_SCHEMA_ID), eventSchemaVersion: positiveSafeIntegerSchema.check( z.refine((value) => value !== FLOW_COMPLETED_EVENT_SCHEMA_VERSION, { message: "Expected a non-v1 Flow Completed schema version.", }), ), messageId: flowActionSucceededMessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowCompletedUnknownVersionPropertiesSchema, }) .check( z.check((context) => { if ( context.value.properties.gx_event_schema_version === context.value.eventSchemaVersion ) { return; } context.issues.push({ code: "custom", input: context.value, message: "Flow Completed property schema version must match its envelope.", path: ["properties", "gx_event_schema_version"], continue: false, }); }), ); export const appEventFlowDismissedV1Schema = z.strictObject({ ...appEventSystemBaseSchema.shape, event: z.literal(FLOW_DISMISSED_EVENT_NAME), eventSchema: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_ID), eventSchemaVersion: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_VERSION), messageId: flowDismissedV1MessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowDismissedV1PropertiesSchema, }); export const appEventFlowDismissedUnknownVersionSchema = z .strictObject({ ...appEventSystemBaseSchema.shape, event: z.literal(FLOW_DISMISSED_EVENT_NAME), eventSchema: z.literal(FLOW_DISMISSED_EVENT_SCHEMA_ID), eventSchemaVersion: positiveSafeIntegerSchema.check( z.refine((value) => value !== FLOW_DISMISSED_EVENT_SCHEMA_VERSION, { message: "Expected a non-v1 Flow Dismissed schema version.", }), ), messageId: flowActionSucceededMessageIdSchema, timestamp: flowActionSucceededTimestampSchema, properties: flowDismissedUnknownVersionPropertiesSchema, }) .check( z.check((context) => { if ( context.value.properties.gx_event_schema_version === context.value.eventSchemaVersion ) { return; } context.issues.push({ code: "custom", input: context.value, message: "Flow Dismissed property schema version must match its envelope.", path: ["properties", "gx_event_schema_version"], continue: false, }); }), ); export type FlowViewedV1AppEvent = z.output; export type FlowViewedUnknownVersionAppEvent = z.output< typeof appEventFlowViewedUnknownVersionSchema >; export type FlowCompletedV1AppEvent = z.output< typeof appEventFlowCompletedV1Schema >; export type FlowCompletedUnknownVersionAppEvent = z.output< typeof appEventFlowCompletedUnknownVersionSchema >; export type FlowDismissedV1AppEvent = z.output< typeof appEventFlowDismissedV1Schema >; export type FlowDismissedUnknownVersionAppEvent = z.output< typeof appEventFlowDismissedUnknownVersionSchema >; // The occurrence seed belongs only at app-event construction. It is not // serialized separately, so the payload schema can enforce its reserved, // nonempty message prefix but cannot re-derive this trusted builder input. export const buildFlowViewedV1AppEvent = (input: { event: FlowViewedV1Contract; flowOccurrenceId: string; timestamp: number; }): FlowViewedV1AppEvent => { const event = flowViewedV1ContractSchema.parse(input.event); const flowOccurrenceId = normalizedRequiredStringSchema.parse( input.flowOccurrenceId, ); return appEventFlowViewedV1Schema.parse({ origin: "system", ...event, messageId: `${flowViewedV1MessageIdPrefix}${flowOccurrenceId}`, timestamp: input.timestamp, }); }; export const buildFlowCompletedV1AppEvent = (input: { event: FlowCompletedV1Contract; timestamp: number; }): FlowCompletedV1AppEvent => { const event = flowCompletedV1ContractSchema.parse(input.event); return appEventFlowCompletedV1Schema.parse({ origin: "system", ...event, messageId: `${flowCompletedV1MessageIdPrefix}${event.properties.gx_flow_completion_id}`, timestamp: input.timestamp, }); }; // See buildFlowViewedV1AppEvent: the same intentionally non-serialized seed // derives dismissal message identity at this construction seam. export const buildFlowDismissedV1AppEvent = (input: { event: FlowDismissedV1Contract; flowOccurrenceId: string; timestamp: number; }): FlowDismissedV1AppEvent => { const event = flowDismissedV1ContractSchema.parse(input.event); const flowOccurrenceId = normalizedRequiredStringSchema.parse( input.flowOccurrenceId, ); return appEventFlowDismissedV1Schema.parse({ origin: "system", ...event, messageId: `${flowDismissedV1MessageIdPrefix}${flowOccurrenceId}`, timestamp: input.timestamp, }); }; const normalizeSystemAppEventOrigin = ( input: T, ): Omit & { origin: "system" } => { const { origin: _origin, ...rest } = input; return { ...rest, origin: "system" as const, }; }; const legacyFlowRunPropertiesSchema = z.strictObject({ gx_flow_run_id: normalizedRequiredStringSchema, }); export const appEventSurveyViewedSchema = z.pipe( z.extend(appEventBaseSchema, { origin: z.optional(z.literal("system")), type: z.optional(z.literal("track")), event: z.literal("Flow Viewed"), references: appEventSurveyReferenceSchema, properties: z.optional(legacyFlowRunPropertiesSchema), }), z.transform(normalizeSystemAppEventOrigin), ); export const appEventFlowDismissedSchema = z.pipe( z.extend(appEventBaseSchema, { origin: z.optional(z.literal("system")), type: z.optional(z.literal("track")), event: z.literal("Flow Dismissed"), references: appEventSurveyReferenceSchema, properties: z.optional(legacyFlowRunPropertiesSchema), }), z.transform(normalizeSystemAppEventOrigin), ); export const appEventSystemTrackSchema = z.extend(appEventSystemBaseSchema, { event: appEventSystemTrackNameSchema, eventSchema: z.optional(z.never()), eventSchemaVersion: z.optional(z.never()), references: appEventSurveyReferenceSchema, // Legacy lifecycle events may carry the renderer/Core run correlation while // remaining unversioned. Versioned event identity is a later migration. properties: z.optional(legacyFlowRunPropertiesSchema), }); const appEventLegacySystemTrackSchema = z.pipe( z.extend(appEventBaseSchema, { origin: z.optional(z.literal("system")), type: z.optional(z.literal("track")), event: appEventSystemTrackNameSchema, eventSchema: z.optional(z.never()), eventSchemaVersion: z.optional(z.never()), references: appEventSurveyReferenceSchema, properties: z.optional(legacyFlowRunPropertiesSchema), }), z.transform(normalizeSystemAppEventOrigin), ); const appEventCustomerPropertiesSchema = protocolJsonObjectSchema.check( z.check((ctx) => { if (!(FLOW_COMPLETION_ID_PROPERTY_KEY in ctx.value)) { return; } ctx.issues.push({ code: "custom", input: ctx.value, message: `${FLOW_COMPLETION_ID_PROPERTY_KEY} is server-owned.`, path: [FLOW_COMPLETION_ID_PROPERTY_KEY], continue: false, }); }), ); const appEventCustomerOccurrenceBaseSchema = z.extend(appEventBaseSchema, { origin: z.literal("customer"), event: appEventCustomerNameSchema, // TODO(migration): Normalize Date values in event properties to ISO strings. // [from=manual-date-property-serialization] [to=protocol-date-property-normalization] [scope=slice] [priority=medium] [impact=medium] [risk=low] properties: z.optional(appEventCustomerPropertiesSchema), }); /** Customer occurrence input. Omitting `type` is the legacy track shape. */ export const appEventCustomerOccurrenceSchema = z.extend( appEventCustomerOccurrenceBaseSchema, { type: z.optional(appEventOccurrenceTypeSchema), }, ); /** Compatibility schema for customer track-only callers. */ export const appEventCustomerTrackSchema = z.extend( appEventCustomerOccurrenceBaseSchema, { type: z.optional(z.literal("track")), }, ); export const appEventTrackSchema = z.union([ appEventLegacySystemTrackSchema, appEventFlowActionSucceededV1Schema, appEventFlowActionSucceededUnknownVersionSchema, appEventFlowViewedV1Schema, appEventFlowViewedUnknownVersionSchema, appEventFlowDismissedV1Schema, appEventFlowDismissedUnknownVersionSchema, appEventCustomerOccurrenceSchema, ]); const appEventDiscriminatedPayloadSchema = z.discriminatedUnion("origin", [ appEventSystemTrackSchema, appEventCustomerOccurrenceSchema, ]); export const appEventPayloadSchema = z.union([ appEventLegacySystemTrackSchema, appEventFlowActionSucceededV1Schema, appEventFlowActionSucceededUnknownVersionSchema, appEventFlowViewedV1Schema, appEventFlowViewedUnknownVersionSchema, appEventFlowDismissedV1Schema, appEventFlowDismissedUnknownVersionSchema, appEventDiscriminatedPayloadSchema, ]); export type AppEventCustomerOccurrence = z.output< typeof appEventCustomerOccurrenceSchema >; export type AppEventOccurrenceInput = z.output; /** Compatibility name for the existing `appEvent.track` ingestion surface. */ export type AppEventTrackInput = AppEventOccurrenceInput; export type AppEventPayload = z.output; export type AppEventFlag = z.output; export type AppEventCapability = z.output;