@servicetitan/journey 4.0.0 → 4.2.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/README.md +35 -0
- package/dist/__tests__/integrations/request-correlation.test.d.ts +2 -0
- package/dist/__tests__/integrations/request-correlation.test.d.ts.map +1 -0
- package/dist/__tests__/sinks/datadog-links.test.d.ts +2 -0
- package/dist/__tests__/sinks/datadog-links.test.d.ts.map +1 -0
- package/dist/__tests__/sinks/kibana.test.d.ts +2 -0
- package/dist/__tests__/sinks/kibana.test.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/request-correlation.d.ts +8 -0
- package/dist/core/request-correlation.d.ts.map +1 -0
- package/dist/core/request-correlation.js +23 -0
- package/dist/core/request-correlation.js.map +1 -0
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +18 -2
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/sanitize.d.ts.map +1 -1
- package/dist/core/sanitize.js +35 -2
- package/dist/core/sanitize.js.map +1 -1
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/axios.d.ts.map +1 -1
- package/dist/integrations/axios.js +2 -1
- package/dist/integrations/axios.js.map +1 -1
- package/dist/integrations/http-report.d.ts +2 -0
- package/dist/integrations/http-report.d.ts.map +1 -1
- package/dist/integrations/http-report.js +17 -2
- package/dist/integrations/http-report.js.map +1 -1
- package/dist/integrations/jquery/instrument.d.ts +2 -0
- package/dist/integrations/jquery/instrument.d.ts.map +1 -1
- package/dist/integrations/jquery/instrument.js +31 -4
- package/dist/integrations/jquery/instrument.js.map +1 -1
- package/dist/integrations/request-correlation.d.ts +5 -0
- package/dist/integrations/request-correlation.d.ts.map +1 -0
- package/dist/integrations/request-correlation.js +64 -0
- package/dist/integrations/request-correlation.js.map +1 -0
- package/dist/sinks/datadog.d.ts.map +1 -1
- package/dist/sinks/datadog.js +26 -0
- package/dist/sinks/datadog.js.map +1 -1
- package/dist/sinks/kibana.d.ts +11 -0
- package/dist/sinks/kibana.d.ts.map +1 -0
- package/dist/sinks/kibana.js +92 -0
- package/dist/sinks/kibana.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/integrations/axios.test.ts +38 -1
- package/src/__tests__/integrations/jquery.test.ts +94 -1
- package/src/__tests__/integrations/request-correlation.test.ts +383 -0
- package/src/__tests__/sinks/datadog-links.test.ts +58 -0
- package/src/__tests__/sinks/kibana.test.ts +267 -0
- package/src/core/index.ts +1 -0
- package/src/core/request-correlation.ts +51 -0
- package/src/core/runtime.ts +7 -0
- package/src/core/sanitize.ts +39 -2
- package/src/core/types.ts +18 -0
- package/src/index.ts +1 -1
- package/src/integrations/axios.ts +1 -0
- package/src/integrations/http-report.ts +33 -14
- package/src/integrations/jquery/instrument.ts +37 -5
- package/src/integrations/request-correlation.ts +93 -0
- package/src/sinks/datadog.ts +21 -0
- package/src/sinks/kibana.ts +140 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { baseJourneyDef, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
|
|
3
|
+
import { excludeJourney, journeyStep } from '../../core';
|
|
4
|
+
import { createJourneyRuntime, moduleRuntime } from '../../core/runtime';
|
|
5
|
+
import type { StepHandle } from '../../core/types';
|
|
6
|
+
import { instrumentFetch } from '../../integrations/fetch';
|
|
7
|
+
import { onHttpComplete, type HttpCompleteReport } from '../../integrations/http-report';
|
|
8
|
+
import { captureRequestCorrelation } from '../../integrations/request-correlation';
|
|
9
|
+
import { sendToDatadog } from '../../sinks/datadog';
|
|
10
|
+
|
|
11
|
+
const emit = jest.fn();
|
|
12
|
+
const addAction = jest.fn();
|
|
13
|
+
const ray = '0123456789abcdef-IAD';
|
|
14
|
+
const epoch = 1_800_000_000_000;
|
|
15
|
+
|
|
16
|
+
function report(step: StepHandle, overrides: Partial<HttpCompleteReport> = {}): HttpCompleteReport {
|
|
17
|
+
return {
|
|
18
|
+
stamp: step.stamp(),
|
|
19
|
+
status: 500,
|
|
20
|
+
durationMs: 250,
|
|
21
|
+
requestKey: '/test',
|
|
22
|
+
url: 'https://go.servicetitan.com/test?secret=do-not-emit',
|
|
23
|
+
response: { headers: { 'cf-ray': ray, 'authorization': 'do-not-emit' } },
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function event() {
|
|
29
|
+
return emit.mock.calls[emit.mock.calls.length - 1][0].journey;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('[journey] request correlation', () => {
|
|
33
|
+
useJourneyTestRuntime(emit);
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.spyOn(Date, 'now').mockReturnValue(epoch);
|
|
36
|
+
addAction.mockClear();
|
|
37
|
+
(globalThis as Record<string, unknown>).DD_RUM = { addAction };
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
delete (globalThis as Record<string, unknown>).DD_RUM;
|
|
41
|
+
jest.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('captures the response before a 5xx synchronously emits, with a usable URL', async () => {
|
|
45
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
46
|
+
onHttpComplete(report(step));
|
|
47
|
+
expect(emit).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
expect(event().outcome).toBe('bad');
|
|
50
|
+
expect(event().reason).toBe('request-error');
|
|
51
|
+
expect(event().steps[0].requests).toEqual([
|
|
52
|
+
{
|
|
53
|
+
rayId: ray.toLowerCase(),
|
|
54
|
+
startedAtEpoch: epoch - 250,
|
|
55
|
+
durationMs: 250,
|
|
56
|
+
status: 500,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
sendToDatadog(emit.mock.calls[0][0]);
|
|
60
|
+
const context = addAction.mock.calls[0][1];
|
|
61
|
+
const request = context.journey.steps[0].requests[0];
|
|
62
|
+
expect(request.request_id).toBe(ray.toLowerCase());
|
|
63
|
+
expect(context.journey.failed_request_id).toBe(ray.toLowerCase());
|
|
64
|
+
expect(context.journey.kibana_url).toBe(request.kibana_url);
|
|
65
|
+
expect(decodeURIComponent(request.kibana_url)).toContain(
|
|
66
|
+
'Request.RequestId: 0123456789abcdef*'
|
|
67
|
+
);
|
|
68
|
+
expect(request.kibana_url).toContain('https://kibana.st.dev/app/discover#/?');
|
|
69
|
+
expect(JSON.stringify(context)).not.toMatch(/secret|authorization|do-not-emit/);
|
|
70
|
+
expect(context.journey.tags).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test.each([
|
|
74
|
+
{ status: 200, durationMs: 5000, reason: 'request-latency' },
|
|
75
|
+
{
|
|
76
|
+
status: 400,
|
|
77
|
+
meta: { httpClientErrorRequests: 'bad' as const },
|
|
78
|
+
reason: 'request-client-error',
|
|
79
|
+
},
|
|
80
|
+
])('captures before %j is scored', async ({ reason, ...overrides }) => {
|
|
81
|
+
await journeyStep(baseJourneyDef, 'save', step => onHttpComplete(report(step, overrides)));
|
|
82
|
+
expect(event().reason).toBe(reason);
|
|
83
|
+
expect(event().steps[0].requests[0].status).toBe(overrides.status);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test.each([200, 400])('does not change the default verdict for status %s', async status => {
|
|
87
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
88
|
+
onHttpComplete(report(step, { status }));
|
|
89
|
+
expect(emit).not.toHaveBeenCalled();
|
|
90
|
+
step.completeJourney();
|
|
91
|
+
});
|
|
92
|
+
expect(event().outcome).toBe('good');
|
|
93
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
94
|
+
expect(event().failedRequest).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test.each([
|
|
98
|
+
{ status: undefined, response: undefined },
|
|
99
|
+
{ response: { headers: {} } },
|
|
100
|
+
{ response: { headers: { 'cf-ray': 'invalid OR Request.User:*' } } },
|
|
101
|
+
{
|
|
102
|
+
response: {
|
|
103
|
+
getResponseHeader() {
|
|
104
|
+
throw new Error('blocked');
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ url: 'https://third-party.example/test' },
|
|
109
|
+
{ url: 'https://go.servicetitan.com.attacker.invalid/test' },
|
|
110
|
+
{ url: '/test', baseURL: 'https://qa.servicetitan.com' },
|
|
111
|
+
{ response: { headers: { 'cf-ray': ray }, url: 'https://third-party.example/test' } },
|
|
112
|
+
])('does not invent a link or change failure scoring for %j', async overrides => {
|
|
113
|
+
await journeyStep(baseJourneyDef, 'save', step => onHttpComplete(report(step, overrides)));
|
|
114
|
+
expect(event().outcome).toBe('bad');
|
|
115
|
+
expect(event().reason).toBe('request-error');
|
|
116
|
+
expect(event().steps[0].requests).toBeUndefined();
|
|
117
|
+
expect(event().failedRequest).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test.each([{ meta: { ignore: true } }, { aborted: true, status: undefined }])(
|
|
121
|
+
'skips ignored/aborted metadata and preserves outcome for %j',
|
|
122
|
+
async overrides => {
|
|
123
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
124
|
+
onHttpComplete(report(step, overrides));
|
|
125
|
+
step.completeJourney();
|
|
126
|
+
});
|
|
127
|
+
expect(event().outcome).toBe('good');
|
|
128
|
+
expect(event().steps[0].requests).toBeUndefined();
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
test('does not record a journey-level ignored endpoint', async () => {
|
|
133
|
+
const config = { ...baseJourneyDef, endpoints: [{ match: '/test', ignore: true }] };
|
|
134
|
+
await journeyStep(config, 'save', step => {
|
|
135
|
+
onHttpComplete(report(step));
|
|
136
|
+
step.failJourney('different-failure');
|
|
137
|
+
});
|
|
138
|
+
expect(event().steps[0].requests).toBeUndefined();
|
|
139
|
+
expect(event().failedRequest).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('captures before a request-specific score closes the journey', async () => {
|
|
143
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
144
|
+
onHttpComplete(report(step, { status: 200, stamp: step.stamp(() => 'bad') }));
|
|
145
|
+
});
|
|
146
|
+
expect(event().outcome).toBe('bad');
|
|
147
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('bounds requests, keeps errors, and deduplicates early/global jQuery observations', async () => {
|
|
151
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
152
|
+
const first = report(step, { status: 400 });
|
|
153
|
+
captureRequestCorrelation(first);
|
|
154
|
+
onHttpComplete(first);
|
|
155
|
+
for (let n = 1; n < 9; n++) {
|
|
156
|
+
onHttpComplete(
|
|
157
|
+
report(step, {
|
|
158
|
+
status: 200,
|
|
159
|
+
response: { headers: { 'cf-ray': n.toString(16).padStart(16, '0') } },
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
onHttpComplete(
|
|
164
|
+
report(step, { response: { headers: { 'cf-ray': 'fedcba9876543210-LAX' } } })
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
const { requests, requestsTruncated } = event().steps[0];
|
|
168
|
+
expect(requests).toHaveLength(5);
|
|
169
|
+
expect(
|
|
170
|
+
requests.filter((item: { rayId: string }) => item.rayId === ray.toLowerCase())
|
|
171
|
+
).toHaveLength(1);
|
|
172
|
+
expect(requests[4].rayId).toBe('fedcba9876543210-lax');
|
|
173
|
+
expect(requestsTruncated).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('an old request cannot attach its ID to a restarted journey', async () => {
|
|
177
|
+
let old: HttpCompleteReport;
|
|
178
|
+
await journeyStep(baseJourneyDef, 'old', step => {
|
|
179
|
+
old = report(step);
|
|
180
|
+
});
|
|
181
|
+
moduleRuntime.startJourney(baseJourneyDef);
|
|
182
|
+
emit.mockClear();
|
|
183
|
+
onHttpComplete(old!);
|
|
184
|
+
expect(emit).not.toHaveBeenCalled();
|
|
185
|
+
await journeyStep(baseJourneyDef, 'new', step => {
|
|
186
|
+
onHttpComplete(report(step, { response: { headers: {} } }));
|
|
187
|
+
});
|
|
188
|
+
expect(event().steps[0].requests).toBeUndefined();
|
|
189
|
+
expect(event().failedRequest).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('routes an explicit stamp to its owning runtime, not a same-named host journey', async () => {
|
|
193
|
+
const foreignEmit = jest.fn();
|
|
194
|
+
const foreign = createJourneyRuntime({ sinks: [foreignEmit] });
|
|
195
|
+
moduleRuntime.startJourney(baseJourneyDef);
|
|
196
|
+
await foreign.journeyStep(baseJourneyDef, 'foreign-save', step =>
|
|
197
|
+
onHttpComplete(report(step))
|
|
198
|
+
);
|
|
199
|
+
expect(foreignEmit.mock.calls[0][0].journey.steps[0].requests).toHaveLength(1);
|
|
200
|
+
expect(moduleRuntime.getOpenJourney(baseJourneyDef.name)).toBeDefined();
|
|
201
|
+
expect(emit).not.toHaveBeenCalled();
|
|
202
|
+
foreign.reset();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('fetch preserves response identity and captures Headers.get before caller settlement', async () => {
|
|
206
|
+
const response = { status: 500, headers: new Headers({ 'cf-ray': ray }) };
|
|
207
|
+
const target = { fetch: jest.fn().mockResolvedValue(response) };
|
|
208
|
+
const restore = instrumentFetch({ target });
|
|
209
|
+
try {
|
|
210
|
+
await journeyStep(baseJourneyDef, 'fetch', async step => {
|
|
211
|
+
expect(
|
|
212
|
+
await target.fetch('https://go.servicetitan.com/test', { stamp: step.stamp() })
|
|
213
|
+
).toBe(response);
|
|
214
|
+
expect(emit).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
217
|
+
} finally {
|
|
218
|
+
restore();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('unrelated concurrent steps receive only their explicitly stamped requests', async () => {
|
|
223
|
+
let settleFirst!: () => void;
|
|
224
|
+
const first = journeyStep({ ...baseJourneyDef, name: 'first' }, 'save', async step => {
|
|
225
|
+
await new Promise<void>(resolve => {
|
|
226
|
+
settleFirst = resolve;
|
|
227
|
+
});
|
|
228
|
+
onHttpComplete(report(step));
|
|
229
|
+
});
|
|
230
|
+
await journeyStep({ ...baseJourneyDef, name: 'second' }, 'save', step => {
|
|
231
|
+
onHttpComplete(
|
|
232
|
+
report(step, { response: { headers: { 'cf-ray': 'fedcba9876543210-IAD' } } })
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
settleFirst();
|
|
236
|
+
await first;
|
|
237
|
+
expect(
|
|
238
|
+
emit.mock.calls.map(call => [
|
|
239
|
+
call[0].journey.name,
|
|
240
|
+
call[0].journey.steps[0].requests[0].rayId,
|
|
241
|
+
])
|
|
242
|
+
).toEqual([
|
|
243
|
+
['second', 'fedcba9876543210-iad'],
|
|
244
|
+
['first', ray.toLowerCase()],
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
test('does not blame a previous HTTP response for a later business failure', async () => {
|
|
248
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
249
|
+
onHttpComplete(report(step, { status: 200 }));
|
|
250
|
+
step.failJourney('business-validation');
|
|
251
|
+
});
|
|
252
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
253
|
+
expect(event().failedRequest).toBeUndefined();
|
|
254
|
+
sendToDatadog(emit.mock.calls[0][0]);
|
|
255
|
+
expect(addAction.mock.calls[0][1].journey.kibana_scope).toBe('related_requests');
|
|
256
|
+
expect(addAction.mock.calls[0][1].journey.failed_request_id).toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('a successful response never replaces an earlier captured failure', async () => {
|
|
260
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
261
|
+
for (let n = 1; n <= 5; n++) {
|
|
262
|
+
captureRequestCorrelation(
|
|
263
|
+
report(step, {
|
|
264
|
+
response: { headers: { 'cf-ray': n.toString(16).padStart(16, '0') } },
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
captureRequestCorrelation(report(step, { status: 200 }));
|
|
269
|
+
step.failJourney('application-error');
|
|
270
|
+
});
|
|
271
|
+
expect(event().steps[0].requests).toHaveLength(5);
|
|
272
|
+
expect(
|
|
273
|
+
event().steps[0].requests.every((item: { status: number }) => item.status === 500)
|
|
274
|
+
).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
test('a later network failure has no top-level link to an earlier successful request', async () => {
|
|
277
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
278
|
+
onHttpComplete(report(step, { status: 200 }));
|
|
279
|
+
onHttpComplete(report(step, { status: undefined, response: undefined }));
|
|
280
|
+
});
|
|
281
|
+
expect(event().reason).toBe('request-error');
|
|
282
|
+
expect(event().failedRequest).toBeUndefined();
|
|
283
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('a slow successful response still gets its failure link when the step history is full', async () => {
|
|
287
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
288
|
+
for (let n = 1; n <= 5; n++) {
|
|
289
|
+
captureRequestCorrelation(
|
|
290
|
+
report(step, {
|
|
291
|
+
response: { headers: { 'cf-ray': n.toString(16).padStart(16, '0') } },
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
onHttpComplete(report(step, { status: 200, durationMs: 5000 }));
|
|
296
|
+
});
|
|
297
|
+
expect(event().reason).toBe('request-latency');
|
|
298
|
+
expect(event().failedRequest.rayId).toBe(ray.toLowerCase());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test.each(['continue', 'exclude'] as const)(
|
|
302
|
+
'preserves the request-specific %s policy',
|
|
303
|
+
async policy => {
|
|
304
|
+
await journeyStep(baseJourneyDef, 'save', step => {
|
|
305
|
+
onHttpComplete(report(step, { stamp: step.stamp(() => policy) }));
|
|
306
|
+
if (policy === 'continue') {
|
|
307
|
+
expect(emit).not.toHaveBeenCalled();
|
|
308
|
+
step.completeJourney();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
expect(event().outcome).toBe(policy === 'continue' ? 'good' : 'excluded');
|
|
312
|
+
expect(event().failedRequest).toBeUndefined();
|
|
313
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('[journey] request links independent of verdict', () => {
|
|
319
|
+
useJourneyTestRuntime(emit);
|
|
320
|
+
beforeEach(() => {
|
|
321
|
+
jest.spyOn(Date, 'now').mockReturnValue(epoch);
|
|
322
|
+
addAction.mockClear();
|
|
323
|
+
(globalThis as Record<string, unknown>).DD_RUM = { addAction };
|
|
324
|
+
});
|
|
325
|
+
afterEach(() => {
|
|
326
|
+
delete (globalThis as Record<string, unknown>).DD_RUM;
|
|
327
|
+
jest.restoreAllMocks();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test.each(['good', 'excluded'] as const)('keeps successful request details on %s', outcome => {
|
|
331
|
+
return journeyStep(baseJourneyDef, 'load', step => {
|
|
332
|
+
onHttpComplete(report(step, { status: 200 }));
|
|
333
|
+
if (outcome === 'good') {
|
|
334
|
+
step.completeJourney();
|
|
335
|
+
} else {
|
|
336
|
+
excludeJourney(baseJourneyDef.name);
|
|
337
|
+
}
|
|
338
|
+
}).then(() => {
|
|
339
|
+
expect(event().outcome).toBe(outcome);
|
|
340
|
+
expect(event().steps[0].requests).toHaveLength(1);
|
|
341
|
+
sendToDatadog(emit.mock.calls[0][0]);
|
|
342
|
+
const j = addAction.mock.calls[0][1].journey;
|
|
343
|
+
expect(j.kibana_scope).toBe('related_requests');
|
|
344
|
+
expect(j.steps[0].requests[0].http_status).toBe(200);
|
|
345
|
+
expect(j.steps[0].requests[0].kibana_url).toContain('https://kibana.st.dev/');
|
|
346
|
+
expect(j.failed_request_id).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('step-error keeps its request link without claiming HTTP scoring caused it', async () => {
|
|
351
|
+
const failure = new Error('application error');
|
|
352
|
+
await expect(
|
|
353
|
+
journeyStep(baseJourneyDef, 'save', step => {
|
|
354
|
+
captureRequestCorrelation(report(step));
|
|
355
|
+
throw failure;
|
|
356
|
+
})
|
|
357
|
+
).rejects.toBe(failure);
|
|
358
|
+
expect(event().reason).toBe('step-error');
|
|
359
|
+
sendToDatadog(emit.mock.calls[0][0]);
|
|
360
|
+
const j = addAction.mock.calls[0][1].journey;
|
|
361
|
+
expect(j.kibana_scope).toBe('related_requests');
|
|
362
|
+
expect(j.failed_request_id).toBeUndefined();
|
|
363
|
+
expect(j.steps[0].requests[0].http_status).toBe(500);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('captures request IDs from verified Enterprise Hub monolith endpoints', async () => {
|
|
367
|
+
await journeyStep(baseJourneyDef, 'save', step =>
|
|
368
|
+
onHttpComplete(
|
|
369
|
+
report(step, {
|
|
370
|
+
url: 'https://test_tenant.eh.go.servicetitan.com/test',
|
|
371
|
+
response: {
|
|
372
|
+
headers: { 'cf-ray': ray },
|
|
373
|
+
responseURL: 'https://test_tenant.eh.go.servicetitan.com/test',
|
|
374
|
+
},
|
|
375
|
+
})
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
expect(event().outcome).toBe('bad');
|
|
379
|
+
expect(event().failedRequest.rayId).toBe(ray.toLowerCase());
|
|
380
|
+
sendToDatadog(emit.mock.calls[0][0]);
|
|
381
|
+
expect(addAction.mock.calls[0][1].journey.kibana_scope).toBe('request');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
* @jest-environment-options {"url": "https://go.servicetitan.com/"}
|
|
4
|
+
*/
|
|
5
|
+
import type { JourneyEvent } from '../../core/types';
|
|
6
|
+
import { sendToDatadog } from '../../sinks/datadog';
|
|
7
|
+
|
|
8
|
+
function sampleEvent(overrides: Partial<JourneyEvent['journey']> = {}): JourneyEvent {
|
|
9
|
+
return {
|
|
10
|
+
journey: {
|
|
11
|
+
name: 'test',
|
|
12
|
+
team: 'test',
|
|
13
|
+
group: 'test',
|
|
14
|
+
service: 'test',
|
|
15
|
+
outcome: 'good',
|
|
16
|
+
durationMs: 120,
|
|
17
|
+
steps: [],
|
|
18
|
+
...overrides,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('[journey] link visibility independent of failure reason', () => {
|
|
24
|
+
const addAction = jest.fn();
|
|
25
|
+
const getUser = jest.fn();
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
addAction.mockClear();
|
|
28
|
+
getUser.mockReset().mockReturnValue({ tenant: 'test_tenant', name: 'test-agent' });
|
|
29
|
+
(globalThis as Record<string, unknown>).DD_RUM = { addAction, getUser };
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
delete (globalThis as Record<string, unknown>).DD_RUM;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test.each(['step-error', 'request-error', 'step-timeout', 'journey-timeout'])(
|
|
36
|
+
'always uses the same visible field for %s with host user context',
|
|
37
|
+
reason => {
|
|
38
|
+
sendToDatadog(sampleEvent({ outcome: 'bad', reason }));
|
|
39
|
+
const j = addAction.mock.calls[0][1].journey;
|
|
40
|
+
expect(j.kibana_url).toContain('https://kibana.st.dev/app/discover');
|
|
41
|
+
expect(j.kibana_scope).toBe('related_logs');
|
|
42
|
+
expect(j.failed_request_id).toBeUndefined();
|
|
43
|
+
expect(j.reason).toBe(reason);
|
|
44
|
+
expect(j.outcome).toBe('bad');
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
test('getUser failure does not lose an event without requests', () => {
|
|
49
|
+
getUser.mockImplementation(() => {
|
|
50
|
+
throw new Error('getUser unavailable');
|
|
51
|
+
});
|
|
52
|
+
expect(() =>
|
|
53
|
+
sendToDatadog(sampleEvent({ reason: 'step-error', outcome: 'bad' }))
|
|
54
|
+
).not.toThrow();
|
|
55
|
+
expect(addAction).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(addAction.mock.calls[0][1].journey.outcome).toBe('bad');
|
|
57
|
+
});
|
|
58
|
+
});
|