@servicetitan/journey 2.0.0 → 2.1.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 (150) hide show
  1. package/dist/__test-utils__/test-runtime.d.ts.map +1 -0
  2. package/dist/__test-utils__/test-runtime.js +47 -0
  3. package/dist/__test-utils__/test-runtime.js.map +1 -0
  4. package/dist/__tests__/core/ambient.test.d.ts.map +1 -0
  5. package/dist/__tests__/core/api.test.d.ts +2 -0
  6. package/dist/__tests__/core/api.test.d.ts.map +1 -0
  7. package/dist/__tests__/core/define-journey.test.d.ts.map +1 -0
  8. package/dist/__tests__/core/requests.test.d.ts.map +1 -0
  9. package/dist/__tests__/core/runtime.test.d.ts.map +1 -0
  10. package/dist/__tests__/core/step.test.d.ts.map +1 -0
  11. package/dist/__tests__/core/timeouts.test.d.ts.map +1 -0
  12. package/dist/__tests__/global.test.d.ts +3 -0
  13. package/dist/__tests__/global.test.d.ts.map +1 -0
  14. package/dist/__tests__/integrations/axios.test.d.ts.map +1 -0
  15. package/dist/__tests__/integrations/context.test.d.ts.map +1 -0
  16. package/dist/__tests__/integrations/expose-app-journey.test.d.ts.map +1 -0
  17. package/dist/__tests__/integrations/fetch.test.d.ts.map +1 -0
  18. package/dist/__tests__/integrations/jquery.test.d.ts.map +1 -0
  19. package/dist/__tests__/integrations/request-key.test.d.ts.map +1 -0
  20. package/dist/__tests__/integrations/scope.test.d.ts.map +1 -0
  21. package/dist/__tests__/sinks/console.test.d.ts +2 -0
  22. package/dist/__tests__/sinks/console.test.d.ts.map +1 -0
  23. package/dist/__tests__/sinks/datadog.test.d.ts.map +1 -0
  24. package/dist/__tests__/sinks/sinks.test.d.ts.map +1 -0
  25. package/dist/config.d.ts +8 -4
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +19 -13
  28. package/dist/config.js.map +1 -1
  29. package/dist/core/{registry.d.ts → api.d.ts} +2 -22
  30. package/dist/core/api.d.ts.map +1 -0
  31. package/dist/core/{registry.js → api.js} +13 -40
  32. package/dist/core/api.js.map +1 -0
  33. package/dist/core/endpoint-policy.d.ts.map +1 -0
  34. package/dist/core/endpoint-policy.js.map +1 -0
  35. package/dist/core/index.d.ts +4 -4
  36. package/dist/core/index.d.ts.map +1 -1
  37. package/dist/core/index.js +3 -2
  38. package/dist/core/index.js.map +1 -1
  39. package/dist/core/runtime.d.ts +35 -7
  40. package/dist/core/runtime.d.ts.map +1 -1
  41. package/dist/core/runtime.js +39 -5
  42. package/dist/core/runtime.js.map +1 -1
  43. package/dist/core/step-tag.d.ts +6 -0
  44. package/dist/core/step-tag.d.ts.map +1 -1
  45. package/dist/core/step-tag.js +7 -0
  46. package/dist/core/step-tag.js.map +1 -1
  47. package/dist/core/types.d.ts +3 -1
  48. package/dist/core/types.d.ts.map +1 -1
  49. package/dist/core/types.js.map +1 -1
  50. package/dist/global.d.ts +67 -0
  51. package/dist/global.d.ts.map +1 -0
  52. package/dist/global.js +116 -0
  53. package/dist/global.js.map +1 -0
  54. package/dist/index.d.ts +8 -3
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +11 -5
  57. package/dist/index.js.map +1 -1
  58. package/dist/integrations/fetch.d.ts +3 -2
  59. package/dist/integrations/fetch.d.ts.map +1 -1
  60. package/dist/integrations/fetch.js.map +1 -1
  61. package/dist/integrations/jquery/index.d.ts +2 -2
  62. package/dist/integrations/jquery/index.d.ts.map +1 -1
  63. package/dist/integrations/jquery/index.js +3 -2
  64. package/dist/integrations/jquery/index.js.map +1 -1
  65. package/dist/integrations/react/scope.d.ts.map +1 -1
  66. package/dist/integrations/react/scope.js +3 -2
  67. package/dist/integrations/react/scope.js.map +1 -1
  68. package/dist/sinks/console.d.ts +11 -0
  69. package/dist/sinks/console.d.ts.map +1 -0
  70. package/dist/sinks/console.js +18 -0
  71. package/dist/sinks/console.js.map +1 -0
  72. package/dist/sinks/datadog.d.ts +12 -1
  73. package/dist/sinks/datadog.d.ts.map +1 -1
  74. package/dist/sinks/datadog.js +28 -7
  75. package/dist/sinks/datadog.js.map +1 -1
  76. package/package.json +2 -2
  77. package/src/{__mocks__ → __test-utils__}/test-runtime.ts +6 -2
  78. package/src/__tests__/config.test.ts +9 -12
  79. package/src/__tests__/{ambient.test.ts → core/ambient.test.ts} +8 -7
  80. package/src/__tests__/{registry.test.ts → core/api.test.ts} +30 -25
  81. package/src/__tests__/{define-journey.test.ts → core/define-journey.test.ts} +5 -4
  82. package/src/__tests__/{requests.test.ts → core/requests.test.ts} +12 -8
  83. package/src/__tests__/{runtime.test.ts → core/runtime.test.ts} +18 -18
  84. package/src/__tests__/{step.test.ts → core/step.test.ts} +16 -18
  85. package/src/__tests__/{timeouts.test.ts → core/timeouts.test.ts} +15 -10
  86. package/src/__tests__/global.test.ts +113 -0
  87. package/src/__tests__/{axios.test.ts → integrations/axios.test.ts} +46 -15
  88. package/src/__tests__/{context.test.tsx → integrations/context.test.tsx} +1 -1
  89. package/src/__tests__/{expose-app-journey.test.ts → integrations/expose-app-journey.test.ts} +2 -2
  90. package/src/__tests__/{fetch.test.ts → integrations/fetch.test.ts} +6 -6
  91. package/src/__tests__/{jquery.test.ts → integrations/jquery.test.ts} +49 -12
  92. package/src/__tests__/{request-key.test.ts → integrations/request-key.test.ts} +1 -1
  93. package/src/__tests__/{scope.test.tsx → integrations/scope.test.tsx} +4 -4
  94. package/src/__tests__/sinks/console.test.ts +98 -0
  95. package/src/__tests__/{datadog.test.ts → sinks/datadog.test.ts} +62 -5
  96. package/src/__tests__/{sinks.test.ts → sinks/sinks.test.ts} +13 -12
  97. package/src/config.ts +20 -12
  98. package/src/core/{registry.ts → api.ts} +12 -65
  99. package/src/core/index.ts +5 -14
  100. package/src/core/runtime.ts +56 -7
  101. package/src/core/step-tag.ts +9 -0
  102. package/src/core/types.ts +3 -1
  103. package/src/global.ts +196 -0
  104. package/src/index.ts +20 -7
  105. package/src/integrations/fetch.ts +2 -2
  106. package/src/integrations/jquery/index.ts +4 -9
  107. package/src/integrations/react/scope.tsx +3 -2
  108. package/src/sinks/console.ts +20 -0
  109. package/src/sinks/datadog.ts +31 -6
  110. package/dist/__mocks__/test-runtime.d.ts.map +0 -1
  111. package/dist/__tests__/ambient.test.d.ts.map +0 -1
  112. package/dist/__tests__/axios.test.d.ts.map +0 -1
  113. package/dist/__tests__/context.test.d.ts.map +0 -1
  114. package/dist/__tests__/datadog.test.d.ts.map +0 -1
  115. package/dist/__tests__/define-journey.test.d.ts.map +0 -1
  116. package/dist/__tests__/expose-app-journey.test.d.ts.map +0 -1
  117. package/dist/__tests__/fetch.test.d.ts.map +0 -1
  118. package/dist/__tests__/jquery.test.d.ts.map +0 -1
  119. package/dist/__tests__/registry.test.d.ts +0 -2
  120. package/dist/__tests__/registry.test.d.ts.map +0 -1
  121. package/dist/__tests__/request-key.test.d.ts.map +0 -1
  122. package/dist/__tests__/requests.test.d.ts.map +0 -1
  123. package/dist/__tests__/runtime.test.d.ts.map +0 -1
  124. package/dist/__tests__/scope.test.d.ts.map +0 -1
  125. package/dist/__tests__/sinks.test.d.ts.map +0 -1
  126. package/dist/__tests__/step.test.d.ts.map +0 -1
  127. package/dist/__tests__/timeouts.test.d.ts.map +0 -1
  128. package/dist/core/registry.d.ts.map +0 -1
  129. package/dist/core/registry.js.map +0 -1
  130. package/dist/endpoint-policy.d.ts.map +0 -1
  131. package/dist/endpoint-policy.js.map +0 -1
  132. /package/dist/{__mocks__ → __test-utils__}/test-runtime.d.ts +0 -0
  133. /package/dist/__tests__/{ambient.test.d.ts → core/ambient.test.d.ts} +0 -0
  134. /package/dist/__tests__/{define-journey.test.d.ts → core/define-journey.test.d.ts} +0 -0
  135. /package/dist/__tests__/{requests.test.d.ts → core/requests.test.d.ts} +0 -0
  136. /package/dist/__tests__/{runtime.test.d.ts → core/runtime.test.d.ts} +0 -0
  137. /package/dist/__tests__/{step.test.d.ts → core/step.test.d.ts} +0 -0
  138. /package/dist/__tests__/{timeouts.test.d.ts → core/timeouts.test.d.ts} +0 -0
  139. /package/dist/__tests__/{axios.test.d.ts → integrations/axios.test.d.ts} +0 -0
  140. /package/dist/__tests__/{context.test.d.ts → integrations/context.test.d.ts} +0 -0
  141. /package/dist/__tests__/{expose-app-journey.test.d.ts → integrations/expose-app-journey.test.d.ts} +0 -0
  142. /package/dist/__tests__/{fetch.test.d.ts → integrations/fetch.test.d.ts} +0 -0
  143. /package/dist/__tests__/{jquery.test.d.ts → integrations/jquery.test.d.ts} +0 -0
  144. /package/dist/__tests__/{request-key.test.d.ts → integrations/request-key.test.d.ts} +0 -0
  145. /package/dist/__tests__/{scope.test.d.ts → integrations/scope.test.d.ts} +0 -0
  146. /package/dist/__tests__/{datadog.test.d.ts → sinks/datadog.test.d.ts} +0 -0
  147. /package/dist/__tests__/{sinks.test.d.ts → sinks/sinks.test.d.ts} +0 -0
  148. /package/dist/{endpoint-policy.d.ts → core/endpoint-policy.d.ts} +0 -0
  149. /package/dist/{endpoint-policy.js → core/endpoint-policy.js} +0 -0
  150. /package/src/{endpoint-policy.ts → core/endpoint-policy.ts} +0 -0
@@ -1,7 +1,8 @@
1
1
  /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { completeJourney, JourneyStepTag, journeyStep, startJourney } from '../core';
4
- import { instrumentAxios, type AxiosLikeInstance } from '../integrations/axios';
2
+ import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
+ import { completeJourney, JourneyStepTag, journeyStep } from '../../core';
4
+ import { moduleRuntime } from '../../core/runtime';
5
+ import { instrumentAxios, type AxiosLikeInstance } from '../../integrations/axios';
5
6
 
6
7
  const emitMock = jest.fn();
7
8
 
@@ -77,7 +78,7 @@ describe('[journey] instrumentAxios', () => {
77
78
  const axios = createAxiosMock();
78
79
  instrumentAxios(axios);
79
80
 
80
- startJourney(def);
81
+ moduleRuntime.startJourney(def);
81
82
  await journeyStep(def.name, 'book-job', () => {
82
83
  const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
83
84
  expect(config.journeyStep).toBeInstanceOf(JourneyStepTag);
@@ -93,11 +94,41 @@ describe('[journey] instrumentAxios', () => {
93
94
  expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
94
95
  });
95
96
 
97
+ test('stamped journeyStep is JSON-safe (AxiosError.toJSON includes config)', async () => {
98
+ const axios = createAxiosMock();
99
+ instrumentAxios(axios);
100
+
101
+ moduleRuntime.startJourney(def);
102
+ await journeyStep(def.name, 'book-job', step => {
103
+ // Ambient stamp — AxiosError.toJSON() embeds config by reference.
104
+ const ambient = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
105
+ expect(ambient.journeyStep).toBeInstanceOf(JourneyStepTag);
106
+ const ambientSerialized = JSON.stringify({
107
+ message: 'Request failed',
108
+ name: 'AxiosError',
109
+ config: ambient,
110
+ status: 500,
111
+ });
112
+ expect(JSON.parse(ambientSerialized).config.journeyStep).toBe('[JourneyStepTag]');
113
+
114
+ // Explicit step.tag() spread into config — same circular risk.
115
+ const tagged = step.tag() as { journeyStep: unknown };
116
+ const explicit = axios.requestHandlers[0]({
117
+ url: '/CallScreen/BookJob',
118
+ journeyStep: tagged.journeyStep,
119
+ });
120
+ expect(explicit.journeyStep).toBe(tagged.journeyStep);
121
+ expect(explicit.journeyStep).toBeInstanceOf(JourneyStepTag);
122
+ expect(JSON.parse(JSON.stringify(explicit)).journeyStep).toBe('[JourneyStepTag]');
123
+ });
124
+ completeJourney(def.name);
125
+ });
126
+
96
127
  test('marks journey bad on 5xx via the rejection interceptor', async () => {
97
128
  const axios = createAxiosMock();
98
129
  instrumentAxios(axios);
99
130
 
100
- startJourney(def);
131
+ moduleRuntime.startJourney(def);
101
132
  await expect(
102
133
  journeyStep(def.name, 'book-job', async () => {
103
134
  const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
@@ -118,7 +149,7 @@ describe('[journey] instrumentAxios', () => {
118
149
  const axios = createAxiosMock();
119
150
  instrumentAxios(axios);
120
151
 
121
- startJourney(def);
152
+ moduleRuntime.startJourney(def);
122
153
  await journeyStep(def.name, 'book-job', async step => {
123
154
  const tagged = step.tag() as { journeyStep: unknown };
124
155
  const config = axios.requestHandlers[0]({
@@ -144,8 +175,8 @@ describe('[journey] instrumentAxios', () => {
144
175
  const axios = createAxiosMock();
145
176
  instrumentAxios(axios);
146
177
 
147
- startJourney(def);
148
- startJourney(other);
178
+ moduleRuntime.startJourney(def);
179
+ moduleRuntime.startJourney(other);
149
180
 
150
181
  let releaseA!: () => void;
151
182
  const gateA = new Promise<void>(r => {
@@ -184,8 +215,8 @@ describe('[journey] instrumentAxios', () => {
184
215
  const axios = createAxiosMock();
185
216
  instrumentAxios(axios);
186
217
 
187
- startJourney(def);
188
- startJourney(other);
218
+ moduleRuntime.startJourney(def);
219
+ moduleRuntime.startJourney(other);
189
220
 
190
221
  let releaseA!: () => void;
191
222
  const gateA = new Promise<void>(r => {
@@ -248,7 +279,7 @@ describe('[journey] instrumentAxios', () => {
248
279
  const axios = createAxiosMock();
249
280
  instrumentAxios(axios);
250
281
 
251
- startJourney(def);
282
+ moduleRuntime.startJourney(def);
252
283
  await journeyStep(def.name, 'book-job', step => {
253
284
  const tagged = step.tag() as { journeyStep: unknown };
254
285
  const config = {
@@ -268,7 +299,7 @@ describe('[journey] instrumentAxios', () => {
268
299
  const axios = createAxiosMock();
269
300
  instrumentAxios(axios);
270
301
 
271
- startJourney({ ...def, requestTimeoutMs: 50 });
302
+ moduleRuntime.startJourney({ ...def, requestTimeoutMs: 50 });
272
303
  await journeyStep(def.name, 'book-job', () => {
273
304
  const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
274
305
  (globalThis.performance.now as jest.Mock).mockReturnValue(1_200);
@@ -284,7 +315,7 @@ describe('[journey] instrumentAxios', () => {
284
315
  const axios = createAxiosMock();
285
316
  instrumentAxios(axios);
286
317
 
287
- startJourney(def);
318
+ moduleRuntime.startJourney(def);
288
319
  await journeyStep(def.name, 'book-job', async () => {
289
320
  const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
290
321
  (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
@@ -305,7 +336,7 @@ describe('[journey] instrumentAxios', () => {
305
336
  const axios = createAxiosMock();
306
337
  instrumentAxios(axios);
307
338
 
308
- startJourney(def);
339
+ moduleRuntime.startJourney(def);
309
340
  await expect(
310
341
  journeyStep(def.name, 'book-job', async () => {
311
342
  const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
@@ -340,7 +371,7 @@ describe('[journey] instrumentAxios', () => {
340
371
  const axios = createAxiosMock();
341
372
  instrumentAxios(axios);
342
373
 
343
- startJourney({
374
+ moduleRuntime.startJourney({
344
375
  ...def,
345
376
  endpoints: [{ match: 'api.example.com/invoices', ignore: true }],
346
377
  });
@@ -5,7 +5,7 @@ import {
5
5
  JourneyNameContext,
6
6
  useJourneyName,
7
7
  useOptionalJourneyName,
8
- } from '../integrations/react/context';
8
+ } from '../../integrations/react/context';
9
9
 
10
10
  function OptionalProbe() {
11
11
  const name = useOptionalJourneyName();
@@ -1,6 +1,6 @@
1
1
  /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { exposeAppJourney, type WindowWithAppJourney } from '../integrations/jquery';
2
+ import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
+ import { exposeAppJourney, type WindowWithAppJourney } from '../../integrations/jquery';
4
4
 
5
5
  const emitMock = jest.fn();
6
6
 
@@ -1,14 +1,14 @@
1
1
  /** @jest-environment jsdom */
2
- import { activeJourneyStep, reportBackendRequest, type JourneyStepRef } from '../core';
3
- import { instrumentFetch, type InstrumentFetchOptions } from '../integrations/fetch';
4
- import { requestKey } from '../integrations/request-key';
2
+ import { activeJourneyStep, reportBackendRequest, type JourneyStepRef } from '../../core';
3
+ import { instrumentFetch, type InstrumentFetchOptions } from '../../integrations/fetch';
4
+ import { requestKey } from '../../integrations/request-key';
5
5
 
6
- jest.mock('../core', () => ({
7
- ...jest.requireActual('../core'),
6
+ jest.mock('../../core', () => ({
7
+ ...jest.requireActual('../../core'),
8
8
  activeJourneyStep: jest.fn(),
9
9
  reportBackendRequest: jest.fn(),
10
10
  }));
11
- jest.mock('../integrations/request-key');
11
+ jest.mock('../../integrations/request-key');
12
12
 
13
13
  describe('[journey] instrumentFetch', () => {
14
14
  const KEY = 'resolved-endpoint-key';
@@ -1,7 +1,8 @@
1
1
  /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { completeJourney, journeyStep, startJourney } from '../core';
4
- import { instrumentJquery } from '../integrations/jquery/instrument';
2
+ import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
+ import { completeJourney, JourneyStepTag, journeyStep } from '../../core';
4
+ import { moduleRuntime } from '../../core/runtime';
5
+ import { instrumentJquery } from '../../integrations/jquery/instrument';
5
6
 
6
7
  const emitMock = jest.fn();
7
8
 
@@ -63,7 +64,7 @@ describe('[journey] instrumentJquery', () => {
63
64
  const $ = createJqueryMock();
64
65
  instrumentJquery($);
65
66
 
66
- startJourney({
67
+ moduleRuntime.startJourney({
67
68
  name: 'jq-idle',
68
69
  team: 'test',
69
70
  group: 'test',
@@ -79,11 +80,47 @@ describe('[journey] instrumentJquery', () => {
79
80
  expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
80
81
  });
81
82
 
83
+ test('stamped journeyStep is JSON-safe on jqXHR and ajax settings', async () => {
84
+ const $ = createJqueryMock();
85
+ instrumentJquery($);
86
+
87
+ moduleRuntime.startJourney({
88
+ name: 'jq-json-safe',
89
+ team: 'test',
90
+ group: 'test',
91
+ service: 'test',
92
+ timeoutMs: 0,
93
+ requestTimeoutMs: 5_000,
94
+ });
95
+
96
+ await journeyStep('jq-json-safe', 'book-job', step => {
97
+ // Ambient stamp lands on jqXHR — common to serialize for logging.
98
+ const ambientXhr: {
99
+ status: number;
100
+ journeyStep?: unknown;
101
+ journeyStart?: number;
102
+ } = { status: 200 };
103
+ const ambientSettings = { url: '/CallScreen/BookJob' };
104
+ for (const prefilter of $.__prefilters) {
105
+ prefilter(ambientSettings, ambientSettings, ambientXhr);
106
+ }
107
+ expect(ambientXhr.journeyStep).toBeInstanceOf(JourneyStepTag);
108
+ expect(JSON.parse(JSON.stringify(ambientXhr)).journeyStep).toBe('[JourneyStepTag]');
109
+
110
+ // Explicit step.tag() spread into ajax settings — same circular risk.
111
+ const tagged = step.tag() as { journeyStep: unknown };
112
+ const settings = { url: '/CallScreen/BookJob', ...tagged };
113
+ expect(settings.journeyStep).toBeInstanceOf(JourneyStepTag);
114
+ expect(JSON.parse(JSON.stringify(settings)).journeyStep).toBe('[JourneyStepTag]');
115
+ });
116
+ completeJourney('jq-json-safe');
117
+ });
118
+
82
119
  test('marks journey bad on 5xx inside a journey step', async () => {
83
120
  const $ = createJqueryMock();
84
121
  instrumentJquery($);
85
122
 
86
- startJourney({
123
+ moduleRuntime.startJourney({
87
124
  name: 'jq-5xx',
88
125
  team: 'test',
89
126
  group: 'test',
@@ -106,7 +143,7 @@ describe('[journey] instrumentJquery', () => {
106
143
  const $ = createJqueryMock();
107
144
  instrumentJquery($);
108
145
 
109
- startJourney({
146
+ moduleRuntime.startJourney({
110
147
  name: 'jq-slow',
111
148
  team: 'test',
112
149
  group: 'test',
@@ -129,7 +166,7 @@ describe('[journey] instrumentJquery', () => {
129
166
  const $ = createJqueryMock();
130
167
  instrumentJquery($);
131
168
 
132
- startJourney({
169
+ moduleRuntime.startJourney({
133
170
  name: 'jq-network',
134
171
  team: 'test',
135
172
  group: 'test',
@@ -151,7 +188,7 @@ describe('[journey] instrumentJquery', () => {
151
188
  const $ = createJqueryMock();
152
189
  instrumentJquery($);
153
190
 
154
- startJourney({
191
+ moduleRuntime.startJourney({
155
192
  name: 'jq-4xx',
156
193
  team: 'test',
157
194
  group: 'test',
@@ -173,7 +210,7 @@ describe('[journey] instrumentJquery', () => {
173
210
  const $ = createJqueryMock();
174
211
  instrumentJquery($);
175
212
 
176
- startJourney({
213
+ moduleRuntime.startJourney({
177
214
  name: 'jq-explicit',
178
215
  team: 'test',
179
216
  group: 'test',
@@ -212,7 +249,7 @@ describe('[journey] instrumentJquery', () => {
212
249
  const $ = createJqueryMock();
213
250
  instrumentJquery($);
214
251
 
215
- startJourney({
252
+ moduleRuntime.startJourney({
216
253
  name: 'jq-no-start',
217
254
  team: 'test',
218
255
  group: 'test',
@@ -269,7 +306,7 @@ describe('[journey] instrumentJquery', () => {
269
306
  return target;
270
307
  }
271
308
 
272
- startJourney({
309
+ moduleRuntime.startJourney({
273
310
  name: 'jq-settings-tag',
274
311
  team: 'test',
275
312
  group: 'test',
@@ -305,7 +342,7 @@ describe('[journey] instrumentJquery', () => {
305
342
  const $ = createJqueryMock();
306
343
  instrumentJquery($);
307
344
 
308
- startJourney({
345
+ moduleRuntime.startJourney({
309
346
  name: 'jq-cross',
310
347
  team: 'test',
311
348
  group: 'test',
@@ -1,5 +1,5 @@
1
1
  /** @jest-environment jsdom */
2
- import { requestKey } from '../integrations/request-key';
2
+ import { requestKey } from '../../integrations/request-key';
3
3
 
4
4
  describe('[journey] requestKey', () => {
5
5
  // Do not redefine window.location — jsdom's property is non-configurable.
@@ -2,10 +2,10 @@
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import { useContext } from 'react';
4
4
 
5
- import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
6
- import { completeJourney, failJourney } from '../core';
7
- import { JourneyNameContext } from '../integrations/react/context';
8
- import { JourneyScope } from '../integrations/react/scope';
5
+ import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
6
+ import { completeJourney, failJourney } from '../../core';
7
+ import { JourneyNameContext } from '../../integrations/react/context';
8
+ import { JourneyScope } from '../../integrations/react/scope';
9
9
 
10
10
  const emitMock = jest.fn();
11
11
 
@@ -0,0 +1,98 @@
1
+ /** @jest-environment jsdom */
2
+ import type { JourneyEvent } from '../../core/types';
3
+ import { JOURNEY_DEBUG_STORAGE_KEY, isJourneyDebugEnabled } from '../../global';
4
+ import { sendToConsole } from '../../sinks/console';
5
+
6
+ function sampleEvent(overrides: Partial<JourneyEvent['journey']> = {}): JourneyEvent {
7
+ return {
8
+ journey: {
9
+ name: 'Book Job',
10
+ team: 'jbce',
11
+ group: 'call-booking',
12
+ service: 'call-screen',
13
+ outcome: 'good',
14
+ durationMs: 120,
15
+ steps: [],
16
+ ...overrides,
17
+ },
18
+ };
19
+ }
20
+
21
+ describe('[journey] sendToConsole / isJourneyDebugEnabled', () => {
22
+ let debugSpy: jest.SpyInstance;
23
+
24
+ beforeEach(() => {
25
+ localStorage.clear();
26
+ debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => undefined);
27
+ });
28
+
29
+ afterEach(() => {
30
+ debugSpy.mockRestore();
31
+ localStorage.clear();
32
+ });
33
+
34
+ describe('when st:journey:debug is not set', () => {
35
+ test('isJourneyDebugEnabled is false', () => {
36
+ expect(isJourneyDebugEnabled()).toBe(false);
37
+ });
38
+
39
+ test('does not log', () => {
40
+ sendToConsole(sampleEvent());
41
+
42
+ expect(debugSpy).not.toHaveBeenCalled();
43
+ });
44
+ });
45
+
46
+ describe('when st:journey:debug is a non-true value', () => {
47
+ beforeEach(() => {
48
+ localStorage.setItem(JOURNEY_DEBUG_STORAGE_KEY, '1');
49
+ });
50
+
51
+ test('isJourneyDebugEnabled is false', () => {
52
+ expect(isJourneyDebugEnabled()).toBe(false);
53
+ });
54
+
55
+ test('does not log', () => {
56
+ sendToConsole(sampleEvent());
57
+
58
+ expect(debugSpy).not.toHaveBeenCalled();
59
+ });
60
+ });
61
+
62
+ describe('when st:journey:debug is true', () => {
63
+ beforeEach(() => {
64
+ localStorage.setItem(JOURNEY_DEBUG_STORAGE_KEY, 'true');
65
+ });
66
+
67
+ test('isJourneyDebugEnabled is true', () => {
68
+ expect(isJourneyDebugEnabled()).toBe(true);
69
+ });
70
+
71
+ test('logs the journey name, outcome, and payload', () => {
72
+ const event = sampleEvent({ outcome: 'bad', reason: 'request-error' });
73
+
74
+ sendToConsole(event);
75
+
76
+ expect(debugSpy).toHaveBeenCalledTimes(1);
77
+ expect(debugSpy).toHaveBeenCalledWith('[journey] Book Job → bad', event.journey);
78
+ });
79
+ });
80
+
81
+ describe('when localStorage throws', () => {
82
+ beforeEach(() => {
83
+ jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
84
+ throw new Error('denied');
85
+ });
86
+ });
87
+
88
+ afterEach(() => {
89
+ jest.restoreAllMocks();
90
+ });
91
+
92
+ test('treats debug as disabled and does not throw', () => {
93
+ expect(isJourneyDebugEnabled()).toBe(false);
94
+ expect(() => sendToConsole(sampleEvent())).not.toThrow();
95
+ expect(debugSpy).not.toHaveBeenCalled();
96
+ });
97
+ });
98
+ });
@@ -1,6 +1,7 @@
1
1
  /** @jest-environment jsdom */
2
2
  const addAction = jest.fn();
3
3
  const importedAddAction = jest.fn();
4
+ const ddRumAddAction = jest.fn();
4
5
 
5
6
  jest.mock('@datadog/browser-rum', () => ({
6
7
  datadogRum: {
@@ -8,10 +9,19 @@ jest.mock('@datadog/browser-rum', () => ({
8
9
  },
9
10
  }));
10
11
 
11
- import type { JourneyEvent } from '../core/types';
12
- import { sendToDatadog, setJourneyRum } from '../sinks/datadog';
12
+ import type { JourneyEvent } from '../../core/types';
13
+ import { ST_JOURNEY_GLOBAL_KEY, type StJourneyGlobal } from '../../global';
14
+ import { normalizeTagValue, sendToDatadog, setJourneyRum } from '../../sinks/datadog';
13
15
 
14
- const RUM_GLOBAL_KEY = '__stJourneyRum';
16
+ const DD_RUM_KEY = 'DD_RUM';
17
+
18
+ function clearJourneyRumFallback(): void {
19
+ const st = (globalThis as Record<string, unknown>)[ST_JOURNEY_GLOBAL_KEY] as
20
+ StJourneyGlobal | undefined;
21
+ if (st) {
22
+ delete st.rum;
23
+ }
24
+ }
15
25
 
16
26
  function sampleEvent(overrides: Partial<JourneyEvent['journey']> = {}): JourneyEvent {
17
27
  return {
@@ -29,10 +39,21 @@ function sampleEvent(overrides: Partial<JourneyEvent['journey']> = {}): JourneyE
29
39
  }
30
40
 
31
41
  describe('[journey] setJourneyRum / sendToDatadog', () => {
42
+ let warnSpy: jest.SpyInstance;
43
+
32
44
  beforeEach(() => {
33
45
  addAction.mockClear();
34
46
  importedAddAction.mockClear();
35
- delete (globalThis as Record<string, unknown>)[RUM_GLOBAL_KEY];
47
+ ddRumAddAction.mockClear();
48
+ clearJourneyRumFallback();
49
+ delete (globalThis as Record<string, unknown>)[DD_RUM_KEY];
50
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
51
+ });
52
+
53
+ afterEach(() => {
54
+ warnSpy.mockRestore();
55
+ clearJourneyRumFallback();
56
+ delete (globalThis as Record<string, unknown>)[DD_RUM_KEY];
36
57
  });
37
58
 
38
59
  describe('without a shared RUM instance', () => {
@@ -47,7 +68,37 @@ describe('[journey] setJourneyRum / sendToDatadog', () => {
47
68
  });
48
69
  });
49
70
 
50
- describe('with setJourneyRum', () => {
71
+ describe('with globalThis.DD_RUM', () => {
72
+ beforeEach(() => {
73
+ (globalThis as Record<string, unknown>)[DD_RUM_KEY] = { addAction: ddRumAddAction };
74
+ });
75
+
76
+ test('prefers DD_RUM over setJourneyRum and the import', () => {
77
+ setJourneyRum({ addAction });
78
+ warnSpy.mockClear();
79
+ sendToDatadog(sampleEvent());
80
+
81
+ expect(ddRumAddAction).toHaveBeenCalledTimes(1);
82
+ expect(addAction).not.toHaveBeenCalled();
83
+ expect(importedAddAction).not.toHaveBeenCalled();
84
+ });
85
+ });
86
+
87
+ describe('when setJourneyRum is called while globalThis.DD_RUM is set', () => {
88
+ test('logs a redundancy warning', () => {
89
+ (globalThis as Record<string, unknown>)[DD_RUM_KEY] = { addAction: ddRumAddAction };
90
+
91
+ setJourneyRum({ addAction });
92
+
93
+ expect(warnSpy).toHaveBeenCalledWith(
94
+ expect.stringContaining(
95
+ 'setJourneyRum() is unnecessary when globalThis.DD_RUM is set'
96
+ )
97
+ );
98
+ });
99
+ });
100
+
101
+ describe('with setJourneyRum (no DD_RUM)', () => {
51
102
  beforeEach(() => {
52
103
  setJourneyRum({ addAction });
53
104
  });
@@ -60,6 +111,7 @@ describe('[journey] setJourneyRum / sendToDatadog', () => {
60
111
 
61
112
  expect(addAction).toHaveBeenCalledTimes(1);
62
113
  expect(importedAddAction).not.toHaveBeenCalled();
114
+ expect(warnSpy).not.toHaveBeenCalled();
63
115
  });
64
116
 
65
117
  test('normalizes identity fields', () => {
@@ -127,6 +179,11 @@ describe('[journey] setJourneyRum / sendToDatadog', () => {
127
179
  expect(addAction.mock.calls[0][1].journey.reason).toBe('step_error');
128
180
  });
129
181
 
182
+ test('normalizeTagValue matches Datadog metric-tag rules', () => {
183
+ expect(normalizeTagValue('Tenant A!')).toBe('tenant_a');
184
+ expect(normalizeTagValue('Foo@@@Bar___')).toBe('foo_bar');
185
+ });
186
+
130
187
  test('maps camelCase fields to Datadog snake_case wire keys', () => {
131
188
  setJourneyRum({ addAction });
132
189
  sendToDatadog(
@@ -1,14 +1,15 @@
1
1
  /** @jest-environment jsdom */
2
- jest.mock('../sinks/datadog', () => ({
2
+ jest.mock('../../sinks/datadog', () => ({
3
3
  sendToDatadog: jest.fn(),
4
4
  }));
5
5
 
6
- import { resetJourney } from '../core';
7
- import { addJourneySink, emitJourneyEvent, setJourneySinks } from '../core/registry';
8
- import type { JourneyEvent } from '../core/types';
9
- import { sendToDatadog } from '../sinks/datadog';
6
+ import { resetJourney } from '../../core';
7
+ import { moduleRuntime } from '../../core/runtime';
8
+ import type { JourneyEvent } from '../../core/types';
9
+ import { sendToConsole } from '../../sinks/console';
10
+ import { sendToDatadog } from '../../sinks/datadog';
10
11
 
11
- describe('[journey] emitJourneyEvent', () => {
12
+ describe('[journey] moduleRuntime sinks', () => {
12
13
  let payload: JourneyEvent;
13
14
 
14
15
  beforeEach(() => {
@@ -28,10 +29,10 @@ describe('[journey] emitJourneyEvent', () => {
28
29
  });
29
30
 
30
31
  afterEach(() => {
31
- resetJourney({ sinks: [sendToDatadog] });
32
+ resetJourney({ sinks: [sendToDatadog, sendToConsole] });
32
33
  });
33
34
 
34
- const subject = () => emitJourneyEvent(payload);
35
+ const subject = () => moduleRuntime.emit(payload);
35
36
 
36
37
  test('forwards the payload to each configured sink', () => {
37
38
  subject();
@@ -44,7 +45,7 @@ describe('[journey] emitJourneyEvent', () => {
44
45
 
45
46
  beforeEach(() => {
46
47
  extra = jest.fn();
47
- addJourneySink(extra);
48
+ moduleRuntime.addSink(extra);
48
49
  });
49
50
 
50
51
  test('invokes every configured sink', () => {
@@ -55,7 +56,7 @@ describe('[journey] emitJourneyEvent', () => {
55
56
  });
56
57
 
57
58
  test('continues to later sinks when one throws', () => {
58
- setJourneySinks([
59
+ moduleRuntime.setSinks([
59
60
  () => {
60
61
  throw new Error('boom');
61
62
  },
@@ -74,7 +75,7 @@ describe('[journey] emitJourneyEvent', () => {
74
75
 
75
76
  beforeEach(() => {
76
77
  only = jest.fn();
77
- setJourneySinks([only]);
78
+ moduleRuntime.setSinks([only]);
78
79
  });
79
80
 
80
81
  test('does not call the previous sinks', () => {
@@ -87,7 +88,7 @@ describe('[journey] emitJourneyEvent', () => {
87
88
 
88
89
  describe('with an empty sink list', () => {
89
90
  beforeEach(() => {
90
- setJourneySinks([]);
91
+ moduleRuntime.setSinks([]);
91
92
  });
92
93
 
93
94
  test('emits nowhere', () => {