@envive-ai/react-hooks 0.3.56 → 0.3.58

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 (95) hide show
  1. package/dist/application/commerce-api.cjs +11 -12
  2. package/dist/application/commerce-api.js +11 -12
  3. package/dist/application/logging/index.cjs +13 -0
  4. package/dist/application/logging/index.d.cts +3 -0
  5. package/dist/application/logging/index.d.ts +3 -0
  6. package/dist/application/logging/index.js +4 -0
  7. package/dist/application/logging/logLevel.cjs +123 -0
  8. package/dist/application/logging/logLevel.d.cts +47 -0
  9. package/dist/application/logging/logLevel.d.ts +47 -0
  10. package/dist/application/logging/logLevel.js +115 -0
  11. package/dist/application/logging/logger.cjs +6 -1
  12. package/dist/application/logging/logger.d.cts +13 -0
  13. package/dist/application/logging/logger.d.ts +13 -0
  14. package/dist/application/logging/logger.js +7 -1
  15. package/dist/atoms/app/index.d.cts +7 -7
  16. package/dist/atoms/app/index.d.ts +6 -6
  17. package/dist/atoms/app/variant.d.ts +6 -6
  18. package/dist/atoms/chat/chatState.d.cts +18 -18
  19. package/dist/atoms/chat/chatState.d.ts +19 -19
  20. package/dist/atoms/chat/form.d.cts +2 -2
  21. package/dist/atoms/chat/form.d.ts +2 -2
  22. package/dist/atoms/chat/index.d.ts +2 -2
  23. package/dist/atoms/chat/lastMessage.d.cts +2 -2
  24. package/dist/atoms/chat/lastMessage.d.ts +2 -2
  25. package/dist/atoms/chat/messageQueue.d.cts +6 -6
  26. package/dist/atoms/chat/messageQueue.d.ts +6 -6
  27. package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
  28. package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
  29. package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
  30. package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
  31. package/dist/atoms/chat/replies.d.cts +1 -1
  32. package/dist/atoms/chat/replies.d.ts +3 -3
  33. package/dist/atoms/chat/suggestions.d.cts +2 -2
  34. package/dist/atoms/chat/suggestions.d.ts +2 -2
  35. package/dist/atoms/envive/enviveConfig.d.ts +14 -14
  36. package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
  37. package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
  38. package/dist/atoms/org/customerService.d.cts +6 -6
  39. package/dist/atoms/org/customerService.d.ts +6 -6
  40. package/dist/atoms/org/graphqlConfig.d.cts +4 -4
  41. package/dist/atoms/org/graphqlConfig.d.ts +5 -5
  42. package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
  43. package/dist/atoms/org/newOrgConfigAtom.d.ts +3 -3
  44. package/dist/atoms/org/orgAnalyticsConfig.d.cts +4 -4
  45. package/dist/atoms/org/orgAnalyticsConfig.d.ts +5 -5
  46. package/dist/atoms/search/chatSearch.d.cts +17 -17
  47. package/dist/atoms/search/chatSearch.d.ts +17 -17
  48. package/dist/atoms/search/searchAPI.d.cts +13 -13
  49. package/dist/atoms/search/searchAPI.d.ts +13 -13
  50. package/dist/atoms/search/types.d.cts +1 -1
  51. package/dist/atoms/search/types.d.ts +1 -1
  52. package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
  53. package/dist/contexts/salesAgentContext/salesAgentService.cjs +2 -2
  54. package/dist/contexts/salesAgentContext/salesAgentService.js +2 -2
  55. package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
  56. package/dist/contexts/types.d.cts +1 -1
  57. package/dist/contexts/types.d.ts +1 -1
  58. package/dist/contexts/typesV3.d.cts +1 -1
  59. package/dist/contexts/typesV3.d.ts +1 -1
  60. package/dist/hooks/ChatToggle/useChatToggle.cjs +10 -8
  61. package/dist/hooks/ChatToggle/useChatToggle.d.cts +4 -3
  62. package/dist/hooks/ChatToggle/useChatToggle.d.ts +4 -3
  63. package/dist/hooks/ChatToggle/useChatToggle.js +11 -9
  64. package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.cts +2 -2
  65. package/dist/hooks/Intersection/useIntersection.cjs +7 -5
  66. package/dist/hooks/Intersection/useIntersection.js +7 -5
  67. package/dist/hooks/Search/useSearchInput.cjs +1 -1
  68. package/dist/hooks/Search/useSearchInput.js +1 -1
  69. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +8 -6
  70. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +8 -6
  71. package/dist/hooks/WidgetInteraction/types.cjs +40 -1
  72. package/dist/hooks/WidgetInteraction/types.d.cts +16 -2
  73. package/dist/hooks/WidgetInteraction/types.d.ts +16 -2
  74. package/dist/hooks/WidgetInteraction/types.js +41 -2
  75. package/dist/hooks/utils.d.cts +1 -1
  76. package/dist/hooks/utils.d.ts +1 -1
  77. package/dist/services/amplitudeService/amplitudeService.cjs +14 -5
  78. package/dist/services/amplitudeService/amplitudeService.js +14 -5
  79. package/package.json +5 -1
  80. package/src/application/__tests__/commerceApiStreaming.test.ts +171 -0
  81. package/src/application/commerce-api.ts +21 -21
  82. package/src/application/logging/__tests__/logger.test.ts +159 -0
  83. package/src/application/logging/index.ts +12 -0
  84. package/src/application/logging/logLevel.ts +162 -0
  85. package/src/application/logging/logger.ts +13 -0
  86. package/src/contexts/salesAgentContext/salesAgentService.ts +1 -1
  87. package/src/contexts/userIdentityContext/__tests__/userIdentityContext.test.tsx +4 -5
  88. package/src/hooks/ChatToggle/__tests__/useChatToggle.test.ts +273 -0
  89. package/src/hooks/ChatToggle/useChatToggle.ts +39 -12
  90. package/src/hooks/Intersection/useIntersection.ts +7 -4
  91. package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +8 -5
  92. package/src/hooks/WidgetInteraction/__tests__/chatTriggerWidget.test.ts +51 -0
  93. package/src/hooks/WidgetInteraction/types.ts +53 -0
  94. package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +76 -0
  95. package/src/services/amplitudeService/amplitudeService.ts +37 -5
@@ -0,0 +1,51 @@
1
+ import { ChatElementDisplayLocationV3 } from '../../../application/models';
2
+ import { CHAT_TRIGGER_WIDGET, WidgetInteractionComponent } from '../types';
3
+
4
+ // System-initiated triggers: the chat opens without the shopper clicking a widget. These must not
5
+ // produce an intentional WIDGET_CLICKED, or they inflate every "meaningful engagement" metric.
6
+ const SYSTEM_TRIGGERS = [
7
+ ChatElementDisplayLocationV3.WINDOW_API_CALL,
8
+ ChatElementDisplayLocationV3.REFERRER_BASED_PROMPT,
9
+ ChatElementDisplayLocationV3.AUTOMATIC_PROMPT,
10
+ ];
11
+
12
+ describe('CHAT_TRIGGER_WIDGET', () => {
13
+ it('maps every display location', () => {
14
+ for (const location of Object.values(ChatElementDisplayLocationV3)) {
15
+ expect(CHAT_TRIGGER_WIDGET[location]).not.toBeUndefined();
16
+ }
17
+ });
18
+
19
+ it('attributes system-initiated triggers to no widget', () => {
20
+ for (const location of SYSTEM_TRIGGERS) {
21
+ expect(CHAT_TRIGGER_WIDGET[location]).toBeNull();
22
+ }
23
+ });
24
+
25
+ it('attributes user-initiated triggers to a widget', () => {
26
+ const userTriggers = Object.values(ChatElementDisplayLocationV3).filter(
27
+ location => !SYSTEM_TRIGGERS.includes(location),
28
+ );
29
+
30
+ for (const location of userTriggers) {
31
+ expect(CHAT_TRIGGER_WIDGET[location]).not.toBeNull();
32
+ }
33
+ });
34
+
35
+ it('attributes a social proof trigger to social_proof, not floating_button', () => {
36
+ // Regression guard: useChatToggle used to hardcode FLOATING_BUTTON for every open,
37
+ // so a suggestion click on the social proof widget was credited to the floating button.
38
+ expect(CHAT_TRIGGER_WIDGET[ChatElementDisplayLocationV3.SOCIAL_PROOF_PRIMARY_BUTTON]).toBe(
39
+ WidgetInteractionComponent.SOCIAL_PROOF,
40
+ );
41
+ expect(CHAT_TRIGGER_WIDGET[ChatElementDisplayLocationV3.SOCIAL_PROOF_TEXT_FIELD]).toBe(
42
+ WidgetInteractionComponent.SOCIAL_PROOF,
43
+ );
44
+ });
45
+
46
+ it('still attributes the floating button to floating_button', () => {
47
+ expect(CHAT_TRIGGER_WIDGET[ChatElementDisplayLocationV3.FLOATING_BUTTON]).toBe(
48
+ WidgetInteractionComponent.FLOATING_BUTTON,
49
+ );
50
+ });
51
+ });
@@ -1,3 +1,4 @@
1
+ import { ChatElementDisplayLocationV3 } from '../../application/models';
1
2
  import { EnviveMetricsEventName, SpiffyMetricsEventName } from '../../contexts/amplitudeContext';
2
3
 
3
4
  export interface WidgetInteractionContext {
@@ -114,6 +115,58 @@ export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionCl
114
115
  [WidgetInteractionType.VARIANT_SELECTOR_DISMISSED]: InteractionClass.NAVIGATIONAL,
115
116
  };
116
117
 
118
+ /**
119
+ * Attribution map: which widget a chat open/close should be credited to, given the surface that
120
+ * triggered it.
121
+ *
122
+ * `null` marks a *system-initiated* trigger — the auto-popup timer, a referrer-based prompt, or a
123
+ * merchant `window` API call. Those are not user clicks, so they must not emit an intentional
124
+ * WIDGET_CLICKED. The chat's own WIDGET_EXPANDED still records that the chat opened, so no signal
125
+ * is lost by skipping them.
126
+ *
127
+ * Keyed exhaustively on purpose: adding a ChatElementDisplayLocationV3 member is a compile error
128
+ * until it is given an attribution here, rather than silently defaulting to the wrong widget.
129
+ */
130
+ export const CHAT_TRIGGER_WIDGET: Record<
131
+ ChatElementDisplayLocationV3,
132
+ WidgetInteractionComponent | null
133
+ > = {
134
+ [ChatElementDisplayLocationV3.PROMPT_CAROUSEL]: WidgetInteractionComponent.SUGGESTION_BAR,
135
+ [ChatElementDisplayLocationV3.TITLED_PROMPT_CAROUSEL]: WidgetInteractionComponent.EMBEDDED_WIDGET,
136
+ [ChatElementDisplayLocationV3.TYPING_ANIMATION]: WidgetInteractionComponent.EMBEDDED_WIDGET,
137
+ [ChatElementDisplayLocationV3.SOCIAL_PROOF_PRIMARY_BUTTON]:
138
+ WidgetInteractionComponent.SOCIAL_PROOF,
139
+ [ChatElementDisplayLocationV3.SOCIAL_PROOF_SECONDARY_BUTTON]:
140
+ WidgetInteractionComponent.SOCIAL_PROOF,
141
+ [ChatElementDisplayLocationV3.SOCIAL_PROOF_TEXT_FIELD]: WidgetInteractionComponent.SOCIAL_PROOF,
142
+ [ChatElementDisplayLocationV3.CHAT_PREVIEW_COMPARISON_PROMPT_BUTTON]:
143
+ WidgetInteractionComponent.POST_INTERACTION_COMPARISON,
144
+ [ChatElementDisplayLocationV3.CHAT_PREVIEW_COMPARISON_TEXT_FIELD]:
145
+ WidgetInteractionComponent.POST_INTERACTION_COMPARISON,
146
+ [ChatElementDisplayLocationV3.CHAT_PREVIEW_PROMPT_BUTTON]:
147
+ WidgetInteractionComponent.CHAT_PREVIEW,
148
+ [ChatElementDisplayLocationV3.CHAT_PREVIEW_TEXT_FIELD]: WidgetInteractionComponent.CHAT_PREVIEW,
149
+ [ChatElementDisplayLocationV3.PROMPT_BUTTON_CAROUSEL_WITH_IMAGE_PROMPT_BUTTON]:
150
+ WidgetInteractionComponent.SINGLE_IMAGE_PROMPT,
151
+ [ChatElementDisplayLocationV3.PROMPT_BUTTON_CAROUSEL_WITH_IMAGE_TEXT_FIELD]:
152
+ WidgetInteractionComponent.SINGLE_IMAGE_PROMPT,
153
+ [ChatElementDisplayLocationV3.FLOATING_BUTTON]: WidgetInteractionComponent.FLOATING_BUTTON,
154
+ [ChatElementDisplayLocationV3.FLOATING_CHAT_OVERLAY]: WidgetInteractionComponent.FLOATING_CHAT,
155
+ [ChatElementDisplayLocationV3.FLOATING_CHAT_CLOSE_BUTTON]:
156
+ WidgetInteractionComponent.FLOATING_CHAT,
157
+ [ChatElementDisplayLocationV3.FLOATING_CHAT_PROMPT_BUTTON]:
158
+ WidgetInteractionComponent.FLOATING_CHAT,
159
+ [ChatElementDisplayLocationV3.FLOATING_CHAT_TEXT_INPUT]: WidgetInteractionComponent.FLOATING_CHAT,
160
+ [ChatElementDisplayLocationV3.PRODUCT_CARD_PROMPT_BUTTON]:
161
+ WidgetInteractionComponent.IMAGE_PROMPT_CARD,
162
+ [ChatElementDisplayLocationV3.PRODUCT_CARD_TEXT_FIELD]:
163
+ WidgetInteractionComponent.IMAGE_PROMPT_CARD,
164
+ // System-initiated — see note above.
165
+ [ChatElementDisplayLocationV3.WINDOW_API_CALL]: null,
166
+ [ChatElementDisplayLocationV3.REFERRER_BASED_PROMPT]: null,
167
+ [ChatElementDisplayLocationV3.AUTOMATIC_PROMPT]: null,
168
+ };
169
+
117
170
  export type URL = {
118
171
  url: string;
119
172
  };
@@ -241,6 +241,82 @@ describe('AmplitudeService', () => {
241
241
  expect(eventProps).toHaveProperty('chat_user_message_input.count', 5);
242
242
  });
243
243
 
244
+ it('should encode supplementary Unicode in widget text events for Amplitude transport', async () => {
245
+ const service = createService();
246
+
247
+ await service.trackEvent({
248
+ eventName: EnviveMetricsEventName.WidgetTextResponse,
249
+ eventProps: {
250
+ widget_text: {
251
+ text_field_placeholder_text: [{ id: 'item-1', value: 'Have a question? Ask away 🙂' }],
252
+ },
253
+ response_id: 'response-1',
254
+ },
255
+ });
256
+
257
+ const eventProps = mockTrack.mock.calls[0][1];
258
+ expect(eventProps.widget_text).toEqual({
259
+ text_field_placeholder_text: [
260
+ {
261
+ id: 'item-1',
262
+ value: '__spiffy_utf8__:Have%20a%20question%3F%20Ask%20away%20%F0%9F%99%82',
263
+ },
264
+ ],
265
+ });
266
+ expect(eventProps.response_id).toBe('response-1');
267
+ expect(eventProps['widget_text_response.widget_text']).toEqual(eventProps.widget_text);
268
+
269
+ await service.trackEvent({
270
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
271
+ eventProps: {
272
+ response_id: 'response-1',
273
+ string_id: 'item-1',
274
+ text: 'Have a question? Ask away 🙂',
275
+ },
276
+ });
277
+
278
+ const clickEventProps = mockTrack.mock.calls[1][1];
279
+ expect(clickEventProps.text).toBe(
280
+ '__spiffy_utf8__:Have%20a%20question%3F%20Ask%20away%20%F0%9F%99%82',
281
+ );
282
+ expect(clickEventProps['widget_text_clicked.text']).toBe(clickEventProps.text);
283
+ });
284
+
285
+ it.each([
286
+ ['emoji', '🙂', '__spiffy_utf8__:%F0%9F%99%82'],
287
+ ['ZWJ sequence', '👩‍💻', '__spiffy_utf8__:%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB'],
288
+ ['flag', '🇧🇷', '__spiffy_utf8__:%F0%9F%87%A7%F0%9F%87%B7'],
289
+ ['skin tone', '👍🏽', '__spiffy_utf8__:%F0%9F%91%8D%F0%9F%8F%BD'],
290
+ ['non-emoji supplementary character', '𝄞', '__spiffy_utf8__:%F0%9D%84%9E'],
291
+ ['BMP text', 'café', 'café'],
292
+ ])('should preserve %s across widget click transport', async (_name, text, expected) => {
293
+ const service = createService();
294
+
295
+ await service.trackEvent({
296
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
297
+ eventProps: {
298
+ response_id: 'response-matrix',
299
+ string_id: 'item-matrix',
300
+ text,
301
+ },
302
+ });
303
+
304
+ const eventProps = mockTrack.mock.calls[0][1];
305
+ expect(eventProps.text).toBe(expected);
306
+ expect(eventProps['widget_text_clicked.text']).toBe(expected);
307
+ });
308
+
309
+ it('should leave supplementary Unicode unchanged for unrelated events', async () => {
310
+ const service = createService();
311
+
312
+ await service.trackEvent({
313
+ eventName: SpiffyMetricsEventName.ChatUserMessageInput,
314
+ eventProps: { message: 'Hello 🙂' },
315
+ });
316
+
317
+ expect(mockTrack.mock.calls[0][1].message).toBe('Hello 🙂');
318
+ });
319
+
244
320
  it('should generate insert_id from event data', async () => {
245
321
  const service = createService();
246
322
 
@@ -19,6 +19,33 @@ export { EnviveMetricsEventName, SpiffyMetricsEventName };
19
19
 
20
20
  const logger = new Logger('amplitudeService');
21
21
 
22
+ // Amplitude's managed BigQuery data can surface supplementary Unicode as U+FFFD.
23
+ // Keep the widget-training payload ASCII-safe without changing the displayed text.
24
+ const UTF8_TRANSPORT_PREFIX = '__spiffy_utf8__:';
25
+
26
+ const containsSupplementaryUnicode = (value: string): boolean =>
27
+ Array.from(value).some(character => (character.codePointAt(0) ?? 0) > 0xffff);
28
+
29
+ const encodeSupplementaryUnicode = (value: unknown): unknown => {
30
+ if (typeof value === 'string') {
31
+ return containsSupplementaryUnicode(value)
32
+ ? `${UTF8_TRANSPORT_PREFIX}${encodeURIComponent(value)}`
33
+ : value;
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return value.map(encodeSupplementaryUnicode);
37
+ }
38
+ if (value !== null && typeof value === 'object') {
39
+ return Object.fromEntries(
40
+ Object.entries(value).map(([key, nestedValue]) => [
41
+ key,
42
+ encodeSupplementaryUnicode(nestedValue),
43
+ ]),
44
+ );
45
+ }
46
+ return value;
47
+ };
48
+
22
49
  export interface TrackEventParams {
23
50
  eventName: SpiffyMetricsEventName | EnviveMetricsEventName;
24
51
  eventProps?: Record<string, unknown>;
@@ -428,10 +455,15 @@ export class AmplitudeService {
428
455
  }
429
456
 
430
457
  const mappedEventProps = AmplitudeService.mapEventProps({ eventName, eventProps });
458
+ const amplitudeEventProps =
459
+ eventName === EnviveMetricsEventName.WidgetTextResponse ||
460
+ eventName === EnviveMetricsEventName.WidgetTextClicked
461
+ ? (encodeSupplementaryUnicode(mappedEventProps) as Record<string, unknown>)
462
+ : mappedEventProps;
431
463
 
432
464
  const eventData = JSON.stringify({
433
465
  eventName,
434
- eventProps: mappedEventProps,
466
+ eventProps: amplitudeEventProps,
435
467
  created_at: new Date().toISOString(),
436
468
  });
437
469
  const encoder = new TextEncoder();
@@ -444,16 +476,16 @@ export class AmplitudeService {
444
476
 
445
477
  logger.logDebug(`amplitude tracking ${decoratedEventName}`, null, {
446
478
  event_name: decoratedEventName,
447
- props: mappedEventProps,
479
+ props: amplitudeEventProps,
448
480
  });
449
481
 
450
482
  this.amplitudeClient.track(
451
483
  decoratedEventName,
452
484
  {
453
485
  ...this.getDefaultTrackingProps(eventName),
454
- ...mappedEventProps,
455
- ...(mappedEventProps
456
- ? this.eventPropsToPrefixedEventProps(eventName, mappedEventProps)
486
+ ...amplitudeEventProps,
487
+ ...(amplitudeEventProps
488
+ ? this.eventPropsToPrefixedEventProps(eventName, amplitudeEventProps)
457
489
  : {}),
458
490
  },
459
491
  {