@getuserfeedback/protocol 0.7.2 → 0.9.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.
Files changed (42) hide show
  1. package/dist/app-event.d.ts +147 -20
  2. package/dist/app-event.d.ts.map +1 -1
  3. package/dist/app-event.js +10 -0
  4. package/dist/flow-assignments.d.ts +7 -4
  5. package/dist/flow-assignments.d.ts.map +1 -1
  6. package/dist/flow-assignments.js +15 -5
  7. package/dist/host/external-id-argument-guards.d.ts +10 -0
  8. package/dist/host/external-id-argument-guards.d.ts.map +1 -0
  9. package/dist/host/external-id-argument-guards.js +27 -0
  10. package/dist/host/index.d.ts +1 -0
  11. package/dist/host/index.d.ts.map +1 -1
  12. package/dist/host/index.js +1 -0
  13. package/dist/host/sdk-types.d.ts +48 -6
  14. package/dist/host/sdk-types.d.ts.map +1 -1
  15. package/dist/identity-type.d.ts +2 -3
  16. package/dist/identity-type.d.ts.map +1 -1
  17. package/dist/identity-type.js +2 -4
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/version-resolution.d.ts +1 -0
  22. package/dist/version-resolution.d.ts.map +1 -1
  23. package/dist/version-resolution.js +1 -0
  24. package/dist/widget-commands.d.ts +23 -1
  25. package/dist/widget-commands.d.ts.map +1 -1
  26. package/dist/widget-commands.js +6 -1
  27. package/dist/widget-config.d.ts +18 -0
  28. package/dist/widget-config.d.ts.map +1 -1
  29. package/dist/widget-config.js +2 -1
  30. package/package.json +1 -1
  31. package/src/app-event.ts +24 -0
  32. package/src/flow-assignments.test.ts +19 -0
  33. package/src/flow-assignments.ts +31 -5
  34. package/src/host/external-id-argument-guards.ts +57 -0
  35. package/src/host/index.ts +1 -0
  36. package/src/host/sdk-types.ts +51 -7
  37. package/src/identity-type.ts +3 -9
  38. package/src/index.ts +4 -1
  39. package/src/protocol-root.test.ts +121 -3
  40. package/src/version-resolution.ts +1 -0
  41. package/src/widget-commands.ts +11 -1
  42. package/src/widget-config.ts +8 -1
package/src/host/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./command-dispatch.js";
6
6
  export * from "./command-envelope.js";
7
7
  export * from "./command-settlement.js";
8
8
  export * from "./constants.js";
9
+ export * from "./external-id-argument-guards.js";
9
10
  export * from "./host-event-contract.js";
10
11
  export * from "./lazy-handle-client.js";
11
12
  export { createCommandRequestId } from "./request-id.js";
@@ -11,8 +11,8 @@ import type { Scope } from "../scopes.js";
11
11
  /**
12
12
  * Identifier types supported by event tracking and identity resolution.
13
13
  *
14
- * Use `custom.<name>` for business-specific identifiers such as
15
- * `custom.shopifyCustomerId` or `custom.salesforceContactId`.
14
+ * Business-specific identifiers are sent as Segment-compatible
15
+ * `context.externalIds` entries on app events or identify options.
16
16
  *
17
17
  * @see https://getuserfeedback.com/docs/guides/identity-resolution
18
18
  */
@@ -23,15 +23,29 @@ export type AppEventIdentityType =
23
23
  | "traits.phone"
24
24
  | "context.device.id"
25
25
  | "context.device.advertisingId"
26
- | "context.device.token"
27
- | `custom.${string}`;
26
+ | "context.device.token";
28
27
 
29
- /** A single identifier, such as a user ID, anonymous ID, email, or custom external ID. */
28
+ /** A single standard identifier, such as a user ID, anonymous ID, email, or device ID. */
30
29
  export type AppEventIdentity = {
31
30
  type: AppEventIdentityType;
32
31
  value: string;
33
32
  };
34
33
 
34
+ /**
35
+ * Segment-compatible external identifier.
36
+ *
37
+ * Only `collection: "users"` participates in person identity resolution today.
38
+ * External IDs help match profiles, but they are not traits by themselves.
39
+ *
40
+ * @see https://getuserfeedback.com/docs/guides/identity-resolution
41
+ */
42
+ export type AppEventExternalId = {
43
+ id: string;
44
+ type: string;
45
+ collection: "users";
46
+ encoding: "none";
47
+ };
48
+
35
49
  /** Page, feature flag, and capability context attached to an event. */
36
50
  export type AppEventContext = {
37
51
  page?: {
@@ -41,6 +55,7 @@ export type AppEventContext = {
41
55
  };
42
56
  locale?: string;
43
57
  userAgent?: string;
58
+ externalIds?: AppEventExternalId[] | undefined;
44
59
  flags?: AppEventFlag[] | undefined;
45
60
  capabilities?: AppEventCapability[] | undefined;
46
61
  };
@@ -367,17 +382,19 @@ export type PrerenderCommandPayload = {
367
382
  hideCloseButton?: boolean | undefined;
368
383
  };
369
384
 
370
- /** Identify with traits only (`client.identify(traits)`). */
385
+ /** Identify with traits only (`client.identify(traits)` or `client.identify(traits, options)`). */
371
386
  export type IdentifyTraitsOnlyCommandPayload = {
372
387
  kind: "identify";
373
388
  traits: Record<string, unknown>;
389
+ options?: IdentifyOptions | undefined;
374
390
  };
375
391
 
376
- /** Identify with userId and optional traits (`client.identify(userId, traits?)`). */
392
+ /** Identify with userId, optional traits, and optional external IDs (`client.identify(userId, traits?, options?)`). */
377
393
  export type IdentifyUserCommandPayload = {
378
394
  kind: "identify";
379
395
  userId: string;
380
396
  traits?: Record<string, unknown> | undefined;
397
+ options?: IdentifyOptions | undefined;
381
398
  };
382
399
 
383
400
  /** Identify command payload: either traits-only or userId+traits shape. */
@@ -385,6 +402,33 @@ export type IdentifyCommandPayload =
385
402
  | IdentifyTraitsOnlyCommandPayload
386
403
  | IdentifyUserCommandPayload;
387
404
 
405
+ /**
406
+ * Optional identity context accepted by `client.identify()`.
407
+ *
408
+ * Use `externalIds` for Segment-compatible identifiers from another system,
409
+ * such as Shopify customer IDs or Salesforce contact IDs. Without a user ID,
410
+ * pass options as `client.identify(traits, options)`, or use
411
+ * `client.identify(traits, undefined, options)` when your call site needs the
412
+ * explicit three-argument form.
413
+ *
414
+ * @see https://getuserfeedback.com/docs/guides/identity-resolution
415
+ */
416
+ export type IdentifyOptions = {
417
+ externalIds?: AppEventExternalId[] | undefined;
418
+ };
419
+
420
+ /**
421
+ * Optional context accepted by `client.track()`.
422
+ *
423
+ * Use `externalIds` when the event carries a Segment-compatible identifier
424
+ * from another system.
425
+ *
426
+ * @see https://getuserfeedback.com/docs/reference/events
427
+ */
428
+ export type TrackOptions = {
429
+ externalIds?: AppEventExternalId[] | undefined;
430
+ };
431
+
388
432
  /**
389
433
  * Command payload for `client.track()`.
390
434
  *
@@ -11,12 +11,7 @@ export const IDENTITY_TYPE_VALUES = [
11
11
  ] as const;
12
12
 
13
13
  export type StandardIdentityTypeValue = (typeof IDENTITY_TYPE_VALUES)[number];
14
- export type CustomIdentityTypeValue = `custom.${string}`;
15
- export type IdentityTypeValue =
16
- | StandardIdentityTypeValue
17
- | CustomIdentityTypeValue;
18
-
19
- const CUSTOM_IDENTITY_TYPE_PATTERN = /^custom\.[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
14
+ export type IdentityTypeValue = StandardIdentityTypeValue;
20
15
 
21
16
  const IDENTITY_TYPE_ALIAS_SUGGESTIONS: Record<
22
17
  string,
@@ -44,8 +39,7 @@ const normalizeIdentityTypeAliasKey = (value: string): string =>
44
39
  export const isIdentityTypeValue = (
45
40
  value: string,
46
41
  ): value is IdentityTypeValue =>
47
- IDENTITY_TYPE_VALUES.some((candidate) => candidate === value) ||
48
- CUSTOM_IDENTITY_TYPE_PATTERN.test(value);
42
+ IDENTITY_TYPE_VALUES.some((candidate) => candidate === value);
49
43
 
50
44
  const parseIdentityTypeValue = (value: string): IdentityTypeValue | null =>
51
45
  isIdentityTypeValue(value) ? value : null;
@@ -69,7 +63,7 @@ export const buildIdentityTypeGuidanceMessage = (value: string): string => {
69
63
  ([from, to]) => `${from} -> ${to}`,
70
64
  ).join(", ");
71
65
 
72
- return `Unsupported identity type ${displayValue}. Use one of: ${IDENTITY_TYPE_VALUES.join(", ")}, or a custom identity type like "custom.shopifyCustomerId". Common mappings: ${mappingExamples}.`;
66
+ return `Unsupported identity type ${displayValue}. Use one of: ${IDENTITY_TYPE_VALUES.join(", ")}. For external IDs, send Segment-compatible context.externalIds entries instead. Common mappings: ${mappingExamples}.`;
73
67
  };
74
68
 
75
69
  const identityTypeInputSchema = z.string().check(
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  type AppEventCapability,
3
+ type AppEventExternalId,
3
4
  type AppEventFlag,
4
5
  type AppEventJsonValue,
5
6
  type AppEventPayload,
@@ -8,6 +9,7 @@ export {
8
9
  appEventCapabilitySchema,
9
10
  appEventContextSchema,
10
11
  appEventCustomerTrackSchema,
12
+ appEventExternalIdSchema,
11
13
  appEventFlagProviderSchema,
12
14
  appEventFlagSchema,
13
15
  appEventFlowDismissedSchema,
@@ -37,6 +39,8 @@ export {
37
39
  type ClaimPendingFlowAssignmentsResponse,
38
40
  claimPendingFlowAssignmentsRequestSchema,
39
41
  claimPendingFlowAssignmentsResponseSchema,
42
+ type FlowAssignmentExternalIdIdentityTypeValue,
43
+ type FlowAssignmentIdentityTypeValue,
40
44
  type FlowAssignmentRecipientRequest,
41
45
  type FlowAssignmentRecord,
42
46
  type FlowAssignmentStatus,
@@ -59,7 +63,6 @@ export {
59
63
  } from "./flow-assignments.js";
60
64
  export {
61
65
  buildIdentityTypeGuidanceMessage,
62
- type CustomIdentityTypeValue,
63
66
  getIdentityTypeSuggestion,
64
67
  IDENTITY_TYPE_VALUES,
65
68
  type IdentityTypeValue,
@@ -161,6 +161,18 @@ describe("@getuserfeedback/protocol root contract", () => {
161
161
  });
162
162
  });
163
163
 
164
+ it("parses theme resolution payloads with optional runtime artifacts", () => {
165
+ expect(
166
+ themeVersionResolutionSchema.parse({
167
+ themeRuntimeArtifactId: "thra_xyz",
168
+ themeVersionId: "tv_xyz",
169
+ }),
170
+ ).toEqual({
171
+ themeRuntimeArtifactId: "thra_xyz",
172
+ themeVersionId: "tv_xyz",
173
+ });
174
+ });
175
+
164
176
  it("exposes public widget command kind validation aligned with public payloads", () => {
165
177
  expect(publicWidgetCommandKindSchema.safeParse("open").success).toBe(true);
166
178
  expect(publicWidgetCommandKindSchema.safeParse("telemetry").success).toBe(
@@ -196,6 +208,16 @@ describe("@getuserfeedback/protocol root contract", () => {
196
208
  traits: {
197
209
  email: "user@example.com",
198
210
  },
211
+ options: {
212
+ externalIds: [
213
+ {
214
+ id: "gid://shopify/Customer/123",
215
+ type: "shopify_customer_id",
216
+ collection: "users",
217
+ encoding: "none",
218
+ },
219
+ ],
220
+ },
199
221
  });
200
222
  const withTraitsOnly = parsePublicCommand({
201
223
  kind: "identify",
@@ -210,6 +232,14 @@ describe("@getuserfeedback/protocol root contract", () => {
210
232
  expect(withUserId.traits).toEqual({
211
233
  email: "user@example.com",
212
234
  });
235
+ expect(withUserId.options?.externalIds).toEqual([
236
+ {
237
+ id: "gid://shopify/Customer/123",
238
+ type: "shopify_customer_id",
239
+ collection: "users",
240
+ encoding: "none",
241
+ },
242
+ ]);
213
243
  }
214
244
 
215
245
  expect(withTraitsOnly.kind).toBe("identify");
@@ -468,9 +498,97 @@ describe("@getuserfeedback/protocol root contract", () => {
468
498
  );
469
499
  });
470
500
 
471
- it("accepts first-class custom app event identity types", () => {
472
- const result = appEventIdentityTypeSchema.parse("custom.shopifyCustomerId");
501
+ it("rejects legacy custom app event identity types", () => {
502
+ expect(() =>
503
+ appEventIdentityTypeSchema.parse("custom.shopifyCustomerId"),
504
+ ).toThrow(/context\.externalIds/);
505
+ });
473
506
 
474
- expect(result).toBe("custom.shopifyCustomerId");
507
+ it("accepts Segment-compatible external IDs in app event context", () => {
508
+ const result = appEventTrackSchema.parse({
509
+ event: "Checkout Started",
510
+ origin: "customer",
511
+ context: {
512
+ externalIds: [
513
+ {
514
+ id: "gid://shopify/123",
515
+ type: "shopify_customer_id",
516
+ collection: "users",
517
+ encoding: "none",
518
+ },
519
+ ],
520
+ },
521
+ });
522
+
523
+ expect(result.context?.externalIds).toEqual([
524
+ {
525
+ id: "gid://shopify/123",
526
+ type: "shopify_customer_id",
527
+ collection: "users",
528
+ encoding: "none",
529
+ },
530
+ ]);
531
+ });
532
+
533
+ it("normalizes external ID values before app event context output", () => {
534
+ const result = appEventTrackSchema.parse({
535
+ event: "Checkout Started",
536
+ origin: "customer",
537
+ context: {
538
+ externalIds: [
539
+ {
540
+ id: " gid://shopify/123 ",
541
+ type: "shopify_customer_id",
542
+ collection: "users",
543
+ encoding: "none",
544
+ },
545
+ ],
546
+ },
547
+ });
548
+
549
+ expect(result.context?.externalIds).toEqual([
550
+ {
551
+ id: "gid://shopify/123",
552
+ type: "shopify_customer_id",
553
+ collection: "users",
554
+ encoding: "none",
555
+ },
556
+ ]);
557
+ });
558
+
559
+ it("caps app event context external IDs to identity cardinality limits", () => {
560
+ expect(() =>
561
+ appEventTrackSchema.parse({
562
+ event: "Checkout Started",
563
+ origin: "customer",
564
+ context: {
565
+ externalIds: Array.from({ length: 21 }, (_, index) => ({
566
+ id: `customer_${index}`,
567
+ type: "shopify_customer_id",
568
+ collection: "users",
569
+ encoding: "none",
570
+ })),
571
+ },
572
+ }),
573
+ ).toThrow();
574
+ });
575
+
576
+ it("rejects uppercase external ID types", () => {
577
+ expect(() =>
578
+ appEventTrackSchema.parse({
579
+ event: "Checkout Started",
580
+ origin: "customer",
581
+ context: {
582
+ externalIds: [
583
+ {
584
+ id: "gid://shopify/123",
585
+ type: "Shopify_Customer_Id",
586
+ collection: "users",
587
+ encoding: "none",
588
+ },
589
+ ],
590
+ },
591
+ }),
592
+ ).toThrow(/lowercase/);
475
593
  });
476
594
  });
@@ -7,6 +7,7 @@ export const flowVersionResolutionSchema = z.object({
7
7
  });
8
8
 
9
9
  export const themeVersionResolutionSchema = z.object({
10
+ themeRuntimeArtifactId: z.optional(versionIdSchema),
10
11
  themeVersionId: versionIdSchema,
11
12
  });
12
13
 
@@ -1,5 +1,8 @@
1
1
  import * as z from "zod/mini";
2
- import { appEventCustomerTrackSchema } from "./app-event.js";
2
+ import {
3
+ appEventCustomerTrackSchema,
4
+ appEventExternalIdSchema,
5
+ } from "./app-event.js";
3
6
  import {
4
7
  type ConfigureOptions,
5
8
  configureOptionsSchema,
@@ -50,15 +53,22 @@ const htmlElementOrNullSchema = z.custom<HTMLElement | null>(
50
53
  );
51
54
 
52
55
  const identifyTraitsSchema = z.record(z.string(), z.unknown());
56
+ const identifyOptionsSchema = z.strictObject({
57
+ externalIds: z.optional(
58
+ z.array(appEventExternalIdSchema).check(z.maxLength(20)),
59
+ ),
60
+ });
53
61
  const identifyPayloadSchema = z.union([
54
62
  z.strictObject({
55
63
  kind: z.literal("identify"),
56
64
  traits: identifyTraitsSchema,
65
+ options: z.optional(identifyOptionsSchema),
57
66
  }),
58
67
  z.strictObject({
59
68
  kind: z.literal("identify"),
60
69
  userId: z.string().check(z.trim(), z.minLength(1)),
61
70
  traits: z.optional(identifyTraitsSchema),
71
+ options: z.optional(identifyOptionsSchema),
62
72
  }),
63
73
  ]);
64
74
 
@@ -1,5 +1,9 @@
1
1
  import * as z from "zod/mini";
2
- import { appEventCapabilitySchema, appEventFlagSchema } from "./app-event.js";
2
+ import {
3
+ appEventCapabilitySchema,
4
+ appEventExternalIdSchema,
5
+ appEventFlagSchema,
6
+ } from "./app-event.js";
3
7
  import { clientMetaSchema } from "./client-meta.js";
4
8
  import { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS } from "./defaults.js";
5
9
  import { runtimeEndpointsSchema } from "./runtime-endpoints.js";
@@ -59,6 +63,9 @@ export const identityConfigInputSchema = z.object({
59
63
  anonymousId: z.optional(z.string().check(z.trim(), z.minLength(1))),
60
64
  userId: z.optional(z.string().check(z.trim(), z.minLength(1))),
61
65
  traits: z.optional(z.record(z.string(), z.unknown())),
66
+ externalIds: z.optional(
67
+ z.array(appEventExternalIdSchema).check(z.maxLength(20)),
68
+ ),
62
69
  });
63
70
 
64
71
  export type IdentityConfigInput = z.output<typeof identityConfigInputSchema>;