@getuserfeedback/protocol 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-event.d.ts +779 -10
- package/dist/app-event.d.ts.map +1 -1
- package/dist/app-event.js +81 -8
- package/dist/flow-assignments.d.ts +7 -4
- package/dist/flow-assignments.d.ts.map +1 -1
- package/dist/flow-assignments.js +15 -5
- package/dist/host/constants.d.ts +2 -2
- package/dist/host/constants.d.ts.map +1 -1
- package/dist/host/constants.js +3 -1
- package/dist/host/external-id-argument-guards.d.ts +10 -0
- package/dist/host/external-id-argument-guards.d.ts.map +1 -0
- package/dist/host/external-id-argument-guards.js +27 -0
- package/dist/host/index.d.ts +1 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -0
- package/dist/host/sdk-types.d.ts +92 -27
- package/dist/host/sdk-types.d.ts.map +1 -1
- package/dist/host/sdk.d.ts.map +1 -1
- package/dist/identity-type.d.ts +2 -1
- package/dist/identity-type.d.ts.map +1 -1
- package/dist/identity-type.js +1 -1
- package/dist/index.d.ts +3 -87
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/widget-commands.d.ts +124 -3
- package/dist/widget-commands.d.ts.map +1 -1
- package/dist/widget-commands.js +10 -0
- package/dist/widget-config.d.ts +23 -5
- package/dist/widget-config.d.ts.map +1 -1
- package/dist/widget-config.js +2 -1
- package/package.json +22 -2
- package/src/app-event.ts +235 -0
- package/src/client-meta.ts +25 -0
- package/src/defaults.ts +4 -0
- package/src/errors.ts +58 -0
- package/src/flow-assignments.test.ts +82 -0
- package/src/flow-assignments.ts +151 -0
- package/src/host/command-dispatch.ts +59 -0
- package/src/host/command-envelope.ts +41 -0
- package/src/host/command-settlement.ts +121 -0
- package/src/host/constants.ts +103 -0
- package/src/host/external-id-argument-guards.ts +57 -0
- package/src/host/host-event-contract.ts +277 -0
- package/src/host/index.ts +14 -0
- package/src/host/lazy-handle-client.ts +207 -0
- package/src/host/request-id.ts +3 -0
- package/src/host/sdk-types.ts +528 -0
- package/src/host/sdk.ts +43 -0
- package/src/host/unique-id.ts +17 -0
- package/src/identity-type.ts +96 -0
- package/src/index.ts +139 -0
- package/src/protocol-root.test.ts +582 -0
- package/src/public-grant-scope.ts +25 -0
- package/src/runtime-endpoints.ts +69 -0
- package/src/scopes.ts +79 -0
- package/src/trpc-envelope.ts +9 -0
- package/src/version-resolution.ts +18 -0
- package/src/widget-commands.ts +162 -0
- package/src/widget-config.ts +164 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AppEventCapability,
|
|
3
|
+
type AppEventExternalId,
|
|
4
|
+
type AppEventFlag,
|
|
5
|
+
type AppEventJsonValue,
|
|
6
|
+
type AppEventPayload,
|
|
7
|
+
type AppEventTrackInput,
|
|
8
|
+
appEventBaseSchema,
|
|
9
|
+
appEventCapabilitySchema,
|
|
10
|
+
appEventContextSchema,
|
|
11
|
+
appEventCustomerTrackSchema,
|
|
12
|
+
appEventExternalIdSchema,
|
|
13
|
+
appEventFlagProviderSchema,
|
|
14
|
+
appEventFlagSchema,
|
|
15
|
+
appEventFlowDismissedSchema,
|
|
16
|
+
appEventIdentitySchema,
|
|
17
|
+
appEventIdentityTypeSchema,
|
|
18
|
+
appEventPayloadSchema,
|
|
19
|
+
appEventSurveyViewedSchema,
|
|
20
|
+
appEventSystemBaseSchema,
|
|
21
|
+
appEventSystemTrackSchema,
|
|
22
|
+
appEventTrackSchema,
|
|
23
|
+
reservedSystemAppEventNames,
|
|
24
|
+
} from "./app-event.js";
|
|
25
|
+
export {
|
|
26
|
+
type ClientMeta,
|
|
27
|
+
clientMetaSchema,
|
|
28
|
+
} from "./client-meta.js";
|
|
29
|
+
export { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS } from "./defaults.js";
|
|
30
|
+
export type { ErrorEvent, ProtocolErrorCode } from "./errors.js";
|
|
31
|
+
export {
|
|
32
|
+
ErrorCategory,
|
|
33
|
+
ErrorCategorySchema,
|
|
34
|
+
ErrorEventSchema,
|
|
35
|
+
ProtocolError,
|
|
36
|
+
} from "./errors.js";
|
|
37
|
+
export {
|
|
38
|
+
type ClaimPendingFlowAssignmentsRequest,
|
|
39
|
+
type ClaimPendingFlowAssignmentsResponse,
|
|
40
|
+
claimPendingFlowAssignmentsRequestSchema,
|
|
41
|
+
claimPendingFlowAssignmentsResponseSchema,
|
|
42
|
+
type FlowAssignmentExternalIdIdentityTypeValue,
|
|
43
|
+
type FlowAssignmentIdentityTypeValue,
|
|
44
|
+
type FlowAssignmentRecipientRequest,
|
|
45
|
+
type FlowAssignmentRecord,
|
|
46
|
+
type FlowAssignmentStatus,
|
|
47
|
+
type FlowAssignmentTargetingContext,
|
|
48
|
+
flowAssignmentRecipientRequestSchema,
|
|
49
|
+
flowAssignmentRecordSchema,
|
|
50
|
+
flowAssignmentStatusSchema,
|
|
51
|
+
flowAssignmentTargetingContextSchema,
|
|
52
|
+
type ListPendingFlowAssignmentsResponse,
|
|
53
|
+
listPendingFlowAssignmentsResponseSchema,
|
|
54
|
+
nullableUpdateFlowAssignmentLifecycleResponseSchema,
|
|
55
|
+
type ReleaseFlowAssignmentClaimRequest,
|
|
56
|
+
type ReleaseFlowAssignmentClaimResponse,
|
|
57
|
+
releaseFlowAssignmentClaimRequestSchema,
|
|
58
|
+
releaseFlowAssignmentClaimResponseSchema,
|
|
59
|
+
type UpdateFlowAssignmentLifecycleRequest,
|
|
60
|
+
type UpdateFlowAssignmentLifecycleResponse,
|
|
61
|
+
updateFlowAssignmentLifecycleRequestSchema,
|
|
62
|
+
updateFlowAssignmentLifecycleResponseSchema,
|
|
63
|
+
} from "./flow-assignments.js";
|
|
64
|
+
export {
|
|
65
|
+
buildIdentityTypeGuidanceMessage,
|
|
66
|
+
getIdentityTypeSuggestion,
|
|
67
|
+
IDENTITY_TYPE_VALUES,
|
|
68
|
+
type IdentityTypeValue,
|
|
69
|
+
isIdentityTypeValue,
|
|
70
|
+
type StandardIdentityTypeValue,
|
|
71
|
+
} from "./identity-type.js";
|
|
72
|
+
export {
|
|
73
|
+
isPublicGrantScope,
|
|
74
|
+
type PublicGrantScope,
|
|
75
|
+
publicGrantScopeIdSchema,
|
|
76
|
+
} from "./public-grant-scope.js";
|
|
77
|
+
export {
|
|
78
|
+
type RuntimeEndpoints,
|
|
79
|
+
resolveApiBaseUrl,
|
|
80
|
+
resolveCoreUrl,
|
|
81
|
+
resolveTrpcBaseUrl,
|
|
82
|
+
runtimeEndpointsSchema,
|
|
83
|
+
} from "./runtime-endpoints.js";
|
|
84
|
+
export {
|
|
85
|
+
getScopeMeta,
|
|
86
|
+
isScope,
|
|
87
|
+
listScopes,
|
|
88
|
+
SCOPE_IDS,
|
|
89
|
+
type Scope,
|
|
90
|
+
scopeIdSchema,
|
|
91
|
+
} from "./scopes.js";
|
|
92
|
+
export {
|
|
93
|
+
type TrpcSuccessResponse,
|
|
94
|
+
trpcSuccessResponseSchema,
|
|
95
|
+
} from "./trpc-envelope.js";
|
|
96
|
+
export {
|
|
97
|
+
type FlowVersionResolution,
|
|
98
|
+
flowVersionResolutionSchema,
|
|
99
|
+
type ThemeVersionResolution,
|
|
100
|
+
themeVersionResolutionSchema,
|
|
101
|
+
} from "./version-resolution.js";
|
|
102
|
+
export {
|
|
103
|
+
type PublicCommandPayload,
|
|
104
|
+
parseConfigureOptions,
|
|
105
|
+
parseInitOptions,
|
|
106
|
+
parsePublicCommand,
|
|
107
|
+
publicCommandPayloadSchema,
|
|
108
|
+
publicWidgetCommandKindSchema,
|
|
109
|
+
} from "./widget-commands.js";
|
|
110
|
+
export {
|
|
111
|
+
type AuthConfigInput,
|
|
112
|
+
type AuthJwtConfigInput,
|
|
113
|
+
authConfigInputSchema,
|
|
114
|
+
authJwtConfigInputSchema,
|
|
115
|
+
type ColorSchemeConfigInput,
|
|
116
|
+
type ConfigureOptions,
|
|
117
|
+
type ConsentConfigInput,
|
|
118
|
+
type CoreInstanceConfig,
|
|
119
|
+
type CoreInstanceConfigInput,
|
|
120
|
+
colorSchemeConfigInputSchema,
|
|
121
|
+
configureOptionsSchema,
|
|
122
|
+
consentConfigInputSchema,
|
|
123
|
+
coreInstanceConfigSchema,
|
|
124
|
+
type DebugConfigInput,
|
|
125
|
+
debugInputSchema,
|
|
126
|
+
type FlagsConfigInput,
|
|
127
|
+
flagsConfigInputSchema,
|
|
128
|
+
type HostCapabilitiesConfigInput,
|
|
129
|
+
hostCapabilitiesConfigInputSchema,
|
|
130
|
+
type IdentityConfigInput,
|
|
131
|
+
INSTANCE_CONFIG_UPDATE_KEYS,
|
|
132
|
+
type InitOptions,
|
|
133
|
+
type InstanceUpdateableConfig,
|
|
134
|
+
identityConfigInputSchema,
|
|
135
|
+
initOptionsSchema,
|
|
136
|
+
instanceUpdateableConfigSchema,
|
|
137
|
+
type TelemetryConfigInput,
|
|
138
|
+
telemetryConfigInputSchema,
|
|
139
|
+
} from "./widget-config.js";
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { FlowDismissedPayload, FlowViewedPayload } from "./host";
|
|
3
|
+
import { createCommandEnvelope } from "./host";
|
|
4
|
+
import {
|
|
5
|
+
appEventCapabilitySchema,
|
|
6
|
+
appEventFlagSchema,
|
|
7
|
+
appEventFlowDismissedSchema,
|
|
8
|
+
appEventIdentityTypeSchema,
|
|
9
|
+
appEventPayloadSchema,
|
|
10
|
+
appEventSurveyViewedSchema,
|
|
11
|
+
appEventTrackSchema,
|
|
12
|
+
DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS,
|
|
13
|
+
flowVersionResolutionSchema,
|
|
14
|
+
getScopeMeta,
|
|
15
|
+
isPublicGrantScope,
|
|
16
|
+
isScope,
|
|
17
|
+
listScopes,
|
|
18
|
+
parseConfigureOptions,
|
|
19
|
+
parseInitOptions,
|
|
20
|
+
parsePublicCommand,
|
|
21
|
+
publicGrantScopeIdSchema,
|
|
22
|
+
publicWidgetCommandKindSchema,
|
|
23
|
+
SCOPE_IDS,
|
|
24
|
+
themeVersionResolutionSchema,
|
|
25
|
+
} from "./index";
|
|
26
|
+
|
|
27
|
+
describe("@getuserfeedback/protocol root contract", () => {
|
|
28
|
+
it("exports the canonical scope ids and metadata helpers", () => {
|
|
29
|
+
expect(SCOPE_IDS).toContain("functionality.storage");
|
|
30
|
+
expect(getScopeMeta("analytics.measurement").description).toContain(
|
|
31
|
+
"analytics events",
|
|
32
|
+
);
|
|
33
|
+
expect(listScopes()).toHaveLength(SCOPE_IDS.length);
|
|
34
|
+
expect(isScope("analytics.measurement")).toBe(true);
|
|
35
|
+
expect(isScope("not-a-scope")).toBe(false);
|
|
36
|
+
expect(isPublicGrantScope("analytics.measurement")).toBe(true);
|
|
37
|
+
expect(isPublicGrantScope("functionality.storage")).toBe(false);
|
|
38
|
+
expect(
|
|
39
|
+
publicGrantScopeIdSchema.safeParse("analytics.measurement").success,
|
|
40
|
+
).toBe(true);
|
|
41
|
+
expect(
|
|
42
|
+
publicGrantScopeIdSchema.safeParse("functionality.storage").success,
|
|
43
|
+
).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("exports the canonical auto-detect color scheme defaults", () => {
|
|
47
|
+
expect(DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS).toEqual([
|
|
48
|
+
"class",
|
|
49
|
+
"data-theme",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("parses widget-config flag and capability inputs via canonical app-event schemas", () => {
|
|
54
|
+
expect(
|
|
55
|
+
appEventFlagSchema.parse({
|
|
56
|
+
key: "new_checkout",
|
|
57
|
+
value: "variant_b",
|
|
58
|
+
variant: "variant_b",
|
|
59
|
+
}),
|
|
60
|
+
).toEqual({
|
|
61
|
+
key: "new_checkout",
|
|
62
|
+
value: "variant_b",
|
|
63
|
+
variant: "variant_b",
|
|
64
|
+
});
|
|
65
|
+
expect(
|
|
66
|
+
appEventCapabilitySchema.parse({
|
|
67
|
+
key: "checkout.drawer",
|
|
68
|
+
source: "host-app",
|
|
69
|
+
}),
|
|
70
|
+
).toEqual({
|
|
71
|
+
key: "checkout.drawer",
|
|
72
|
+
source: "host-app",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("parses init options from the root contract", () => {
|
|
77
|
+
const result = parseInitOptions({
|
|
78
|
+
apiKey: "test-key",
|
|
79
|
+
colorScheme: {
|
|
80
|
+
autoDetectColorScheme: ["data-theme"],
|
|
81
|
+
},
|
|
82
|
+
defaultConsent: ["analytics.measurement"],
|
|
83
|
+
capabilities: ["gx.command.open.v1"],
|
|
84
|
+
flags: [
|
|
85
|
+
{
|
|
86
|
+
key: "new_checkout",
|
|
87
|
+
value: "variant_b",
|
|
88
|
+
variant: "variant_b",
|
|
89
|
+
provider: { name: "launchdarkly", environment: "production" },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
hostCapabilities: [{ key: "checkout.drawer", source: "host-app" }],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(result.apiKey).toBe("test-key");
|
|
96
|
+
expect(result.defaultConsent).toEqual(["analytics.measurement"]);
|
|
97
|
+
expect(result.capabilities).toEqual(["gx.command.open.v1"]);
|
|
98
|
+
expect(result.flags?.[0]).toMatchObject({
|
|
99
|
+
key: "new_checkout",
|
|
100
|
+
value: "variant_b",
|
|
101
|
+
});
|
|
102
|
+
expect(result.hostCapabilities?.[0]).toEqual({
|
|
103
|
+
key: "checkout.drawer",
|
|
104
|
+
source: "host-app",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("accepts essential scopes in consent array inputs (CMP compatibility)", () => {
|
|
109
|
+
const result = parseInitOptions({
|
|
110
|
+
apiKey: "test-key",
|
|
111
|
+
defaultConsent: ["functionality.storage", "analytics.measurement"],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.defaultConsent).toEqual([
|
|
115
|
+
"functionality.storage",
|
|
116
|
+
"analytics.measurement",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("parses configure options from the root contract", () => {
|
|
121
|
+
const result = parseConfigureOptions({
|
|
122
|
+
auth: {
|
|
123
|
+
jwt: {
|
|
124
|
+
token: "jwt-token",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
consent: ["ads.storage"],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.auth?.jwt?.token).toBe("jwt-token");
|
|
131
|
+
expect(result.consent).toEqual(["ads.storage"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("parses public commands from the root contract", () => {
|
|
135
|
+
const result = parsePublicCommand({
|
|
136
|
+
kind: "configure",
|
|
137
|
+
opts: {
|
|
138
|
+
consent: ["analytics.storage"],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.kind).toBe("configure");
|
|
143
|
+
if (result.kind === "configure") {
|
|
144
|
+
expect(result.opts.consent).toEqual(["analytics.storage"]);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("parses public customer track commands from the root contract", () => {
|
|
149
|
+
const result = parsePublicCommand({
|
|
150
|
+
kind: "track",
|
|
151
|
+
origin: "customer",
|
|
152
|
+
event: "Checkout Started",
|
|
153
|
+
properties: { plan: "starter" },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result).toEqual({
|
|
157
|
+
kind: "track",
|
|
158
|
+
origin: "customer",
|
|
159
|
+
event: "Checkout Started",
|
|
160
|
+
properties: { plan: "starter" },
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("exposes public widget command kind validation aligned with public payloads", () => {
|
|
165
|
+
expect(publicWidgetCommandKindSchema.safeParse("open").success).toBe(true);
|
|
166
|
+
expect(publicWidgetCommandKindSchema.safeParse("telemetry").success).toBe(
|
|
167
|
+
false,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("strips legacy open.containerRequirement for enum values any and hostOnly", () => {
|
|
172
|
+
for (const containerRequirement of ["any", "hostOnly"] as const) {
|
|
173
|
+
const result = parsePublicCommand({
|
|
174
|
+
kind: "open",
|
|
175
|
+
flowId: "sur_123",
|
|
176
|
+
flowHandleId: "handle_open_123",
|
|
177
|
+
containerRequirement,
|
|
178
|
+
hideCloseButton: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result.kind).toBe("open");
|
|
182
|
+
expect(result).toEqual({
|
|
183
|
+
kind: "open",
|
|
184
|
+
flowId: "sur_123",
|
|
185
|
+
flowHandleId: "handle_open_123",
|
|
186
|
+
hideCloseButton: true,
|
|
187
|
+
});
|
|
188
|
+
expect("containerRequirement" in result).toBe(false);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("parses public identify commands from the root contract", () => {
|
|
193
|
+
const withUserId = parsePublicCommand({
|
|
194
|
+
kind: "identify",
|
|
195
|
+
userId: "user_123",
|
|
196
|
+
traits: {
|
|
197
|
+
email: "user@example.com",
|
|
198
|
+
},
|
|
199
|
+
options: {
|
|
200
|
+
externalIds: [
|
|
201
|
+
{
|
|
202
|
+
id: "gid://shopify/Customer/123",
|
|
203
|
+
type: "shopify_customer_id",
|
|
204
|
+
collection: "users",
|
|
205
|
+
encoding: "none",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const withTraitsOnly = parsePublicCommand({
|
|
211
|
+
kind: "identify",
|
|
212
|
+
traits: {
|
|
213
|
+
plan: "pro",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(withUserId.kind).toBe("identify");
|
|
218
|
+
if (withUserId.kind === "identify" && "userId" in withUserId) {
|
|
219
|
+
expect(withUserId.userId).toBe("user_123");
|
|
220
|
+
expect(withUserId.traits).toEqual({
|
|
221
|
+
email: "user@example.com",
|
|
222
|
+
});
|
|
223
|
+
expect(withUserId.options?.externalIds).toEqual([
|
|
224
|
+
{
|
|
225
|
+
id: "gid://shopify/Customer/123",
|
|
226
|
+
type: "shopify_customer_id",
|
|
227
|
+
collection: "users",
|
|
228
|
+
encoding: "none",
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
expect(withTraitsOnly.kind).toBe("identify");
|
|
234
|
+
if (withTraitsOnly.kind === "identify" && !("userId" in withTraitsOnly)) {
|
|
235
|
+
expect(withTraitsOnly.traits).toEqual({
|
|
236
|
+
plan: "pro",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("builds canonical public command envelopes from the host contract", () => {
|
|
242
|
+
const envelope = createCommandEnvelope({
|
|
243
|
+
command: {
|
|
244
|
+
kind: "configure",
|
|
245
|
+
opts: {
|
|
246
|
+
consent: ["analytics.storage"],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
instanceId: "instance_123",
|
|
250
|
+
clientMeta: {
|
|
251
|
+
loader: "sdk",
|
|
252
|
+
transport: "snippet",
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(envelope.version).toBe("1");
|
|
257
|
+
expect(envelope.command.kind).toBe("configure");
|
|
258
|
+
expect(envelope.instanceId).toBe("instance_123");
|
|
259
|
+
expect(envelope.idempotencyKey).toBe(envelope.requestId);
|
|
260
|
+
expect(envelope.clientMeta).toEqual({
|
|
261
|
+
loader: "sdk",
|
|
262
|
+
transport: "snippet",
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("normalizes blank optional envelope identifiers", () => {
|
|
267
|
+
const envelope = createCommandEnvelope({
|
|
268
|
+
command: {
|
|
269
|
+
kind: "configure",
|
|
270
|
+
opts: {
|
|
271
|
+
consent: ["analytics.storage"],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
instanceId: " ",
|
|
275
|
+
requestId: " request_123 ",
|
|
276
|
+
idempotencyKey: " ",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(envelope.requestId).toBe("request_123");
|
|
280
|
+
expect(envelope.idempotencyKey).toBe("request_123");
|
|
281
|
+
expect("instanceId" in envelope).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("rejects internal navigation context commands from the public contract", () => {
|
|
285
|
+
expect(() =>
|
|
286
|
+
parsePublicCommand({
|
|
287
|
+
kind: "hostContextUpdated",
|
|
288
|
+
context: {
|
|
289
|
+
url: "https://example.com/pricing",
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
).toThrow();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("parses public API flow and theme version resolution payloads", () => {
|
|
296
|
+
expect(
|
|
297
|
+
flowVersionResolutionSchema.parse({ flowVersionId: "fv_abc" }),
|
|
298
|
+
).toEqual({ flowVersionId: "fv_abc" });
|
|
299
|
+
expect(
|
|
300
|
+
themeVersionResolutionSchema.parse({ themeVersionId: "tv_xyz" }),
|
|
301
|
+
).toEqual({ themeVersionId: "tv_xyz" });
|
|
302
|
+
expect(() =>
|
|
303
|
+
flowVersionResolutionSchema.parse({ flowVersionId: " " }),
|
|
304
|
+
).toThrow();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("parses app event track and payload shapes", () => {
|
|
308
|
+
const track = appEventTrackSchema.parse({
|
|
309
|
+
event: "Flow Viewed",
|
|
310
|
+
references: { surveyId: "sur_1" },
|
|
311
|
+
identities: [{ type: "anonymousId", value: "anon" }],
|
|
312
|
+
context: {
|
|
313
|
+
flags: [
|
|
314
|
+
{
|
|
315
|
+
key: "new_checkout",
|
|
316
|
+
value: "variant_b",
|
|
317
|
+
variant: "variant_b",
|
|
318
|
+
provider: {
|
|
319
|
+
name: "launchdarkly",
|
|
320
|
+
project: "mobile",
|
|
321
|
+
environment: "production",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
capabilities: [{ key: "new_checkout" }],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
expect(track.event).toBe("Flow Viewed");
|
|
329
|
+
expect(track.origin).toBe("system");
|
|
330
|
+
expect(track.references.surveyId).toBe("sur_1");
|
|
331
|
+
expect(track.context?.flags?.[0]).toMatchObject({
|
|
332
|
+
key: "new_checkout",
|
|
333
|
+
value: "variant_b",
|
|
334
|
+
});
|
|
335
|
+
expect(track.context?.capabilities?.[0]).toEqual({
|
|
336
|
+
key: "new_checkout",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const viewed = appEventPayloadSchema.parse({
|
|
340
|
+
origin: "system",
|
|
341
|
+
event: "Flow Viewed",
|
|
342
|
+
references: { surveyId: "sur_2" },
|
|
343
|
+
});
|
|
344
|
+
expect(viewed.event).toBe("Flow Viewed");
|
|
345
|
+
|
|
346
|
+
const customer = appEventTrackSchema.parse({
|
|
347
|
+
origin: "customer",
|
|
348
|
+
event: "Checkout Started",
|
|
349
|
+
properties: {
|
|
350
|
+
plan: "starter",
|
|
351
|
+
step: 1,
|
|
352
|
+
renewal: null,
|
|
353
|
+
items: ["seat", { count: 2 }],
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
expect(customer).toEqual({
|
|
357
|
+
origin: "customer",
|
|
358
|
+
event: "Checkout Started",
|
|
359
|
+
properties: {
|
|
360
|
+
plan: "starter",
|
|
361
|
+
step: 1,
|
|
362
|
+
renewal: null,
|
|
363
|
+
items: ["seat", { count: 2 }],
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
for (const properties of [
|
|
368
|
+
{ callback: () => undefined },
|
|
369
|
+
{ amount: Number.NaN },
|
|
370
|
+
{ amount: Number.POSITIVE_INFINITY },
|
|
371
|
+
{ orderId: BigInt(1) },
|
|
372
|
+
{ renewedAt: new globalThis.Date("2026-05-08T00:00:00.000Z") },
|
|
373
|
+
]) {
|
|
374
|
+
expect(() =>
|
|
375
|
+
appEventTrackSchema.parse({
|
|
376
|
+
origin: "customer",
|
|
377
|
+
event: "Checkout Started",
|
|
378
|
+
properties,
|
|
379
|
+
}),
|
|
380
|
+
).toThrow();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
expect(() =>
|
|
384
|
+
appEventTrackSchema.parse({
|
|
385
|
+
event: "Unknown",
|
|
386
|
+
references: { surveyId: "sur_1" },
|
|
387
|
+
}),
|
|
388
|
+
).toThrow();
|
|
389
|
+
expect(() =>
|
|
390
|
+
appEventTrackSchema.parse({
|
|
391
|
+
origin: "customer",
|
|
392
|
+
event: "Flow Viewed",
|
|
393
|
+
}),
|
|
394
|
+
).toThrow();
|
|
395
|
+
expect(() =>
|
|
396
|
+
appEventTrackSchema.parse({
|
|
397
|
+
origin: "customer",
|
|
398
|
+
event: "flow viewed",
|
|
399
|
+
}),
|
|
400
|
+
).toThrow();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("keeps flag value validation separate from track properties", () => {
|
|
404
|
+
expect(
|
|
405
|
+
appEventFlagSchema.parse({
|
|
406
|
+
key: "new_checkout",
|
|
407
|
+
value: {
|
|
408
|
+
variant: "treatment",
|
|
409
|
+
enabled: true,
|
|
410
|
+
rollout: 50,
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
).toEqual({
|
|
414
|
+
key: "new_checkout",
|
|
415
|
+
value: {
|
|
416
|
+
variant: "treatment",
|
|
417
|
+
enabled: true,
|
|
418
|
+
rollout: 50,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(() =>
|
|
423
|
+
appEventFlagSchema.parse({
|
|
424
|
+
key: "new_checkout",
|
|
425
|
+
value: { callback: () => undefined },
|
|
426
|
+
}),
|
|
427
|
+
).toThrow();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("keeps exported system app event schemas compatible with legacy no-origin inputs", () => {
|
|
431
|
+
const viewed = appEventSurveyViewedSchema.parse({
|
|
432
|
+
event: "Flow Viewed",
|
|
433
|
+
references: { surveyId: "sur_1" },
|
|
434
|
+
});
|
|
435
|
+
const dismissed = appEventFlowDismissedSchema.parse({
|
|
436
|
+
event: "Flow Dismissed",
|
|
437
|
+
references: { surveyId: "sur_2" },
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(viewed.origin).toBe("system");
|
|
441
|
+
expect(dismissed.origin).toBe("system");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("keeps legacy system app event SDK input types origin-optional", () => {
|
|
445
|
+
const viewed: FlowViewedPayload = {
|
|
446
|
+
event: "Flow Viewed",
|
|
447
|
+
references: { surveyId: "sur_1" },
|
|
448
|
+
};
|
|
449
|
+
const dismissed: FlowDismissedPayload = {
|
|
450
|
+
event: "Flow Dismissed",
|
|
451
|
+
references: { surveyId: "sur_2" },
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
expect(viewed.origin).toBeUndefined();
|
|
455
|
+
expect(dismissed.origin).toBeUndefined();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("suggests canonical identity types for app event aliases", () => {
|
|
459
|
+
expect(() =>
|
|
460
|
+
appEventTrackSchema.parse({
|
|
461
|
+
event: "Flow Viewed",
|
|
462
|
+
references: { surveyId: "sur_1" },
|
|
463
|
+
identities: [{ type: "email", value: "person@example.com" }],
|
|
464
|
+
}),
|
|
465
|
+
).toThrow(/traits\.email/);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("keeps identity type guidance on invalid app event identity values", () => {
|
|
469
|
+
expect(() => appEventIdentityTypeSchema.safeParse("email")).not.toThrow();
|
|
470
|
+
|
|
471
|
+
const result = appEventIdentityTypeSchema.safeParse("email");
|
|
472
|
+
|
|
473
|
+
expect(result.success).toBe(false);
|
|
474
|
+
if (!result.success) {
|
|
475
|
+
expect(result.error.issues).toEqual([
|
|
476
|
+
expect.objectContaining({
|
|
477
|
+
code: "custom",
|
|
478
|
+
message:
|
|
479
|
+
'Unsupported identity type "email". Use "traits.email" instead.',
|
|
480
|
+
}),
|
|
481
|
+
]);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
expect(() => appEventIdentityTypeSchema.parse("email")).toThrow(
|
|
485
|
+
/traits\.email/,
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("rejects legacy custom app event identity types", () => {
|
|
490
|
+
expect(() =>
|
|
491
|
+
appEventIdentityTypeSchema.parse("custom.shopifyCustomerId"),
|
|
492
|
+
).toThrow(/context\.externalIds/);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("accepts Segment-compatible external IDs in app event context", () => {
|
|
496
|
+
const result = appEventTrackSchema.parse({
|
|
497
|
+
event: "Checkout Started",
|
|
498
|
+
origin: "customer",
|
|
499
|
+
context: {
|
|
500
|
+
externalIds: [
|
|
501
|
+
{
|
|
502
|
+
id: "gid://shopify/123",
|
|
503
|
+
type: "shopify_customer_id",
|
|
504
|
+
collection: "users",
|
|
505
|
+
encoding: "none",
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(result.context?.externalIds).toEqual([
|
|
512
|
+
{
|
|
513
|
+
id: "gid://shopify/123",
|
|
514
|
+
type: "shopify_customer_id",
|
|
515
|
+
collection: "users",
|
|
516
|
+
encoding: "none",
|
|
517
|
+
},
|
|
518
|
+
]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("normalizes external ID values before app event context output", () => {
|
|
522
|
+
const result = appEventTrackSchema.parse({
|
|
523
|
+
event: "Checkout Started",
|
|
524
|
+
origin: "customer",
|
|
525
|
+
context: {
|
|
526
|
+
externalIds: [
|
|
527
|
+
{
|
|
528
|
+
id: " gid://shopify/123 ",
|
|
529
|
+
type: "shopify_customer_id",
|
|
530
|
+
collection: "users",
|
|
531
|
+
encoding: "none",
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(result.context?.externalIds).toEqual([
|
|
538
|
+
{
|
|
539
|
+
id: "gid://shopify/123",
|
|
540
|
+
type: "shopify_customer_id",
|
|
541
|
+
collection: "users",
|
|
542
|
+
encoding: "none",
|
|
543
|
+
},
|
|
544
|
+
]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("caps app event context external IDs to identity cardinality limits", () => {
|
|
548
|
+
expect(() =>
|
|
549
|
+
appEventTrackSchema.parse({
|
|
550
|
+
event: "Checkout Started",
|
|
551
|
+
origin: "customer",
|
|
552
|
+
context: {
|
|
553
|
+
externalIds: Array.from({ length: 21 }, (_, index) => ({
|
|
554
|
+
id: `customer_${index}`,
|
|
555
|
+
type: "shopify_customer_id",
|
|
556
|
+
collection: "users",
|
|
557
|
+
encoding: "none",
|
|
558
|
+
})),
|
|
559
|
+
},
|
|
560
|
+
}),
|
|
561
|
+
).toThrow();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("rejects uppercase external ID types", () => {
|
|
565
|
+
expect(() =>
|
|
566
|
+
appEventTrackSchema.parse({
|
|
567
|
+
event: "Checkout Started",
|
|
568
|
+
origin: "customer",
|
|
569
|
+
context: {
|
|
570
|
+
externalIds: [
|
|
571
|
+
{
|
|
572
|
+
id: "gid://shopify/123",
|
|
573
|
+
type: "Shopify_Customer_Id",
|
|
574
|
+
collection: "users",
|
|
575
|
+
encoding: "none",
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
}),
|
|
580
|
+
).toThrow(/lowercase/);
|
|
581
|
+
});
|
|
582
|
+
});
|