@getuserfeedback/protocol 0.7.2 → 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.
Files changed (38) 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 +24 -4
  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/widget-commands.d.ts +23 -1
  22. package/dist/widget-commands.d.ts.map +1 -1
  23. package/dist/widget-commands.js +6 -1
  24. package/dist/widget-config.d.ts +18 -0
  25. package/dist/widget-config.d.ts.map +1 -1
  26. package/dist/widget-config.js +2 -1
  27. package/package.json +1 -1
  28. package/src/app-event.ts +24 -0
  29. package/src/flow-assignments.test.ts +19 -0
  30. package/src/flow-assignments.ts +31 -5
  31. package/src/host/external-id-argument-guards.ts +57 -0
  32. package/src/host/index.ts +1 -0
  33. package/src/host/sdk-types.ts +27 -5
  34. package/src/identity-type.ts +3 -9
  35. package/src/index.ts +4 -1
  36. package/src/protocol-root.test.ts +109 -3
  37. package/src/widget-commands.ts +11 -1
  38. package/src/widget-config.ts +8 -1
@@ -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,26 @@ 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
+ */
39
+ export type AppEventExternalId = {
40
+ id: string;
41
+ type: string;
42
+ collection: "users";
43
+ encoding: "none";
44
+ };
45
+
35
46
  /** Page, feature flag, and capability context attached to an event. */
36
47
  export type AppEventContext = {
37
48
  page?: {
@@ -41,6 +52,7 @@ export type AppEventContext = {
41
52
  };
42
53
  locale?: string;
43
54
  userAgent?: string;
55
+ externalIds?: AppEventExternalId[] | undefined;
44
56
  flags?: AppEventFlag[] | undefined;
45
57
  capabilities?: AppEventCapability[] | undefined;
46
58
  };
@@ -371,6 +383,7 @@ export type PrerenderCommandPayload = {
371
383
  export type IdentifyTraitsOnlyCommandPayload = {
372
384
  kind: "identify";
373
385
  traits: Record<string, unknown>;
386
+ options?: IdentifyOptions | undefined;
374
387
  };
375
388
 
376
389
  /** Identify with userId and optional traits (`client.identify(userId, traits?)`). */
@@ -378,6 +391,7 @@ export type IdentifyUserCommandPayload = {
378
391
  kind: "identify";
379
392
  userId: string;
380
393
  traits?: Record<string, unknown> | undefined;
394
+ options?: IdentifyOptions | undefined;
381
395
  };
382
396
 
383
397
  /** Identify command payload: either traits-only or userId+traits shape. */
@@ -385,6 +399,14 @@ export type IdentifyCommandPayload =
385
399
  | IdentifyTraitsOnlyCommandPayload
386
400
  | IdentifyUserCommandPayload;
387
401
 
402
+ export type IdentifyOptions = {
403
+ externalIds?: AppEventExternalId[] | undefined;
404
+ };
405
+
406
+ export type TrackOptions = {
407
+ externalIds?: AppEventExternalId[] | undefined;
408
+ };
409
+
388
410
  /**
389
411
  * Command payload for `client.track()`.
390
412
  *
@@ -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,
@@ -196,6 +196,16 @@ describe("@getuserfeedback/protocol root contract", () => {
196
196
  traits: {
197
197
  email: "user@example.com",
198
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
+ },
199
209
  });
200
210
  const withTraitsOnly = parsePublicCommand({
201
211
  kind: "identify",
@@ -210,6 +220,14 @@ describe("@getuserfeedback/protocol root contract", () => {
210
220
  expect(withUserId.traits).toEqual({
211
221
  email: "user@example.com",
212
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
+ ]);
213
231
  }
214
232
 
215
233
  expect(withTraitsOnly.kind).toBe("identify");
@@ -468,9 +486,97 @@ describe("@getuserfeedback/protocol root contract", () => {
468
486
  );
469
487
  });
470
488
 
471
- it("accepts first-class custom app event identity types", () => {
472
- const result = appEventIdentityTypeSchema.parse("custom.shopifyCustomerId");
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
+ });
473
520
 
474
- expect(result).toBe("custom.shopifyCustomerId");
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/);
475
581
  });
476
582
  });
@@ -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>;