@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.
Files changed (66) hide show
  1. package/README.md +35 -0
  2. package/dist/__tests__/integrations/request-correlation.test.d.ts +2 -0
  3. package/dist/__tests__/integrations/request-correlation.test.d.ts.map +1 -0
  4. package/dist/__tests__/sinks/datadog-links.test.d.ts +2 -0
  5. package/dist/__tests__/sinks/datadog-links.test.d.ts.map +1 -0
  6. package/dist/__tests__/sinks/kibana.test.d.ts +2 -0
  7. package/dist/__tests__/sinks/kibana.test.d.ts.map +1 -0
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/request-correlation.d.ts +8 -0
  12. package/dist/core/request-correlation.d.ts.map +1 -0
  13. package/dist/core/request-correlation.js +23 -0
  14. package/dist/core/request-correlation.js.map +1 -0
  15. package/dist/core/runtime.d.ts.map +1 -1
  16. package/dist/core/runtime.js +18 -2
  17. package/dist/core/runtime.js.map +1 -1
  18. package/dist/core/sanitize.d.ts.map +1 -1
  19. package/dist/core/sanitize.js +35 -2
  20. package/dist/core/sanitize.js.map +1 -1
  21. package/dist/core/types.d.ts +17 -0
  22. package/dist/core/types.d.ts.map +1 -1
  23. package/dist/core/types.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/integrations/axios.d.ts.map +1 -1
  28. package/dist/integrations/axios.js +2 -1
  29. package/dist/integrations/axios.js.map +1 -1
  30. package/dist/integrations/http-report.d.ts +2 -0
  31. package/dist/integrations/http-report.d.ts.map +1 -1
  32. package/dist/integrations/http-report.js +17 -2
  33. package/dist/integrations/http-report.js.map +1 -1
  34. package/dist/integrations/jquery/instrument.d.ts +2 -0
  35. package/dist/integrations/jquery/instrument.d.ts.map +1 -1
  36. package/dist/integrations/jquery/instrument.js +31 -4
  37. package/dist/integrations/jquery/instrument.js.map +1 -1
  38. package/dist/integrations/request-correlation.d.ts +5 -0
  39. package/dist/integrations/request-correlation.d.ts.map +1 -0
  40. package/dist/integrations/request-correlation.js +64 -0
  41. package/dist/integrations/request-correlation.js.map +1 -0
  42. package/dist/sinks/datadog.d.ts.map +1 -1
  43. package/dist/sinks/datadog.js +26 -0
  44. package/dist/sinks/datadog.js.map +1 -1
  45. package/dist/sinks/kibana.d.ts +11 -0
  46. package/dist/sinks/kibana.d.ts.map +1 -0
  47. package/dist/sinks/kibana.js +92 -0
  48. package/dist/sinks/kibana.js.map +1 -0
  49. package/package.json +2 -2
  50. package/src/__tests__/integrations/axios.test.ts +38 -1
  51. package/src/__tests__/integrations/jquery.test.ts +94 -1
  52. package/src/__tests__/integrations/request-correlation.test.ts +383 -0
  53. package/src/__tests__/sinks/datadog-links.test.ts +58 -0
  54. package/src/__tests__/sinks/kibana.test.ts +267 -0
  55. package/src/core/index.ts +1 -0
  56. package/src/core/request-correlation.ts +51 -0
  57. package/src/core/runtime.ts +7 -0
  58. package/src/core/sanitize.ts +39 -2
  59. package/src/core/types.ts +18 -0
  60. package/src/index.ts +1 -1
  61. package/src/integrations/axios.ts +1 -0
  62. package/src/integrations/http-report.ts +33 -14
  63. package/src/integrations/jquery/instrument.ts +37 -5
  64. package/src/integrations/request-correlation.ts +93 -0
  65. package/src/sinks/datadog.ts +21 -0
  66. package/src/sinks/kibana.ts +140 -0
@@ -0,0 +1,64 @@
1
+ import { isMonolithLogOrigin, MAX_CORRELATED_REQUESTS, sanitizeRequestCorrelation } from '../core/request-correlation';
2
+ import { canRouteHttpToJourney, resolveJourneyEngine } from '../core/runtime';
3
+ const capturedResponses = new WeakMap();
4
+ /** Observation only. Safe to run before jQuery invokes application callbacks. */ export function captureRequestCorrelation(report) {
5
+ try {
6
+ var _step, _requests;
7
+ var _report_stamp, _report_stamp1, _report_meta, _globalThis_location, _response_request, _response_headers, _response_headers1;
8
+ const step = (_report_stamp = report.stamp) === null || _report_stamp === void 0 ? void 0 : _report_stamp.step;
9
+ if (!(step === null || step === void 0 ? void 0 : step.journey) || step.journey.closed || !canRouteHttpToJourney(step.journey) || ((_report_stamp1 = report.stamp) === null || _report_stamp1 === void 0 ? void 0 : _report_stamp1.ignore) || ((_report_meta = report.meta) === null || _report_meta === void 0 ? void 0 : _report_meta.ignore) || report.aborted || !report.url || resolveJourneyEngine(step.journey).shouldIgnoreRequest(report.requestKey)) {
10
+ return;
11
+ }
12
+ // Other environments/backends have not verified this log-field/data-view contract.
13
+ const pageUrl = (_globalThis_location = globalThis.location) === null || _globalThis_location === void 0 ? void 0 : _globalThis_location.href;
14
+ const base = report.baseURL ? new URL(report.baseURL, pageUrl).href : pageUrl;
15
+ const url = new URL(report.url, base);
16
+ if (!isMonolithLogOrigin(url.href)) {
17
+ return;
18
+ }
19
+ const response = report.response;
20
+ if (!response || typeof response !== 'object') {
21
+ return;
22
+ }
23
+ const captured = capturedResponses.get(response);
24
+ if (captured) {
25
+ var _step_requests;
26
+ return ((_step_requests = step.requests) === null || _step_requests === void 0 ? void 0 : _step_requests.includes(captured)) ? captured : undefined;
27
+ }
28
+ const finalUrl = response.url || response.responseURL || ((_response_request = response.request) === null || _response_request === void 0 ? void 0 : _response_request.responseURL);
29
+ if (finalUrl && !isMonolithLogOrigin(finalUrl)) {
30
+ return;
31
+ }
32
+ const rayId = typeof response.getResponseHeader === 'function' ? response.getResponseHeader('cf-ray') : typeof ((_response_headers = response.headers) === null || _response_headers === void 0 ? void 0 : _response_headers.get) === 'function' ? response.headers.get('cf-ray') : (_response_headers1 = response.headers) === null || _response_headers1 === void 0 ? void 0 : _response_headers1['cf-ray'];
33
+ if (typeof rayId !== 'string') {
34
+ return;
35
+ }
36
+ const request = sanitizeRequestCorrelation({
37
+ rayId,
38
+ startedAtEpoch: Date.now() - report.durationMs,
39
+ durationMs: report.durationMs,
40
+ status: report.status
41
+ });
42
+ if (!request) {
43
+ return;
44
+ }
45
+ capturedResponses.set(response, request);
46
+ (_requests = (_step = step).requests) !== null && _requests !== void 0 ? _requests : _step.requests = [];
47
+ const requests = step.requests;
48
+ if (requests.length >= MAX_CORRELATED_REQUESTS) {
49
+ step.requestsTruncated = true;
50
+ // Keep errors when later successes arrive. A new error replaces a success first.
51
+ const replace = requests.findIndex((item)=>item.status < 400);
52
+ if (replace < 0 && request.status < 400) {
53
+ return request;
54
+ }
55
+ requests.splice(replace < 0 ? 0 : replace, 1);
56
+ }
57
+ requests.push(request);
58
+ return request;
59
+ } catch (unused) {
60
+ // Missing/blocked headers and unusual transport objects must not affect requests.
61
+ }
62
+ }
63
+
64
+ //# sourceMappingURL=request-correlation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/integrations/request-correlation.ts"],"sourcesContent":["import {\n isMonolithLogOrigin,\n MAX_CORRELATED_REQUESTS,\n sanitizeRequestCorrelation,\n} from '../core/request-correlation';\nimport { canRouteHttpToJourney, resolveJourneyEngine } from '../core/runtime';\nimport type { HttpCompleteReport } from './http-report';\nimport type { JourneyRequestEvent } from '../core/types';\n\nconst capturedResponses = new WeakMap<object, JourneyRequestEvent>();\n\ninterface ResponseHeaders {\n headers?: { 'cf-ray'?: unknown; 'get'?(name: string): unknown };\n url?: string;\n responseURL?: string;\n request?: { responseURL?: string };\n getResponseHeader?(name: string): unknown;\n}\n\n/** Observation only. Safe to run before jQuery invokes application callbacks. */\nexport function captureRequestCorrelation(\n report: HttpCompleteReport\n): JourneyRequestEvent | undefined {\n try {\n const step = report.stamp?.step;\n if (\n !step?.journey ||\n step.journey.closed ||\n !canRouteHttpToJourney(step.journey) ||\n report.stamp?.ignore ||\n report.meta?.ignore ||\n report.aborted ||\n !report.url ||\n resolveJourneyEngine(step.journey).shouldIgnoreRequest(report.requestKey)\n ) {\n return;\n }\n\n // Other environments/backends have not verified this log-field/data-view contract.\n const pageUrl = globalThis.location?.href;\n const base = report.baseURL ? new URL(report.baseURL, pageUrl).href : pageUrl;\n const url = new URL(report.url, base);\n if (!isMonolithLogOrigin(url.href)) {\n return;\n }\n const response = report.response as ResponseHeaders | undefined;\n if (!response || typeof response !== 'object') {\n return;\n }\n const captured = capturedResponses.get(response);\n if (captured) {\n return step.requests?.includes(captured) ? captured : undefined;\n }\n const finalUrl = response.url || response.responseURL || response.request?.responseURL;\n if (finalUrl && !isMonolithLogOrigin(finalUrl)) {\n return;\n }\n const rayId =\n typeof response.getResponseHeader === 'function'\n ? response.getResponseHeader('cf-ray')\n : typeof response.headers?.get === 'function'\n ? response.headers.get('cf-ray')\n : response.headers?.['cf-ray'];\n if (typeof rayId !== 'string') {\n return;\n }\n const request = sanitizeRequestCorrelation({\n rayId,\n startedAtEpoch: Date.now() - report.durationMs,\n durationMs: report.durationMs,\n status: report.status as number,\n });\n if (!request) {\n return;\n }\n capturedResponses.set(response, request);\n step.requests ??= [];\n const requests = step.requests;\n if (requests.length >= MAX_CORRELATED_REQUESTS) {\n step.requestsTruncated = true;\n // Keep errors when later successes arrive. A new error replaces a success first.\n const replace = requests.findIndex(item => item.status < 400);\n if (replace < 0 && request.status < 400) {\n return request;\n }\n requests.splice(replace < 0 ? 0 : replace, 1);\n }\n requests.push(request);\n return request;\n } catch {\n // Missing/blocked headers and unusual transport objects must not affect requests.\n }\n}\n"],"names":["isMonolithLogOrigin","MAX_CORRELATED_REQUESTS","sanitizeRequestCorrelation","canRouteHttpToJourney","resolveJourneyEngine","capturedResponses","WeakMap","captureRequestCorrelation","report","step","globalThis","response","stamp","journey","closed","ignore","meta","aborted","url","shouldIgnoreRequest","requestKey","pageUrl","location","href","base","baseURL","URL","captured","get","requests","includes","undefined","finalUrl","responseURL","request","rayId","getResponseHeader","headers","startedAtEpoch","Date","now","durationMs","status","set","length","requestsTruncated","replace","findIndex","item","splice","push"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"datadog.d.ts","sourceRoot":"","sources":["../../src/sinks/datadog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,eAAe,CAAC;AAEpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAkBrD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAQhD;AAsBD,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAyBzD"}
1
+ {"version":3,"file":"datadog.d.ts","sourceRoot":"","sources":["../../src/sinks/datadog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,eAAe,CAAC;AAGpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAmBrD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAQhD;AAkCD,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAgCzD"}
@@ -1,5 +1,6 @@
1
1
  import { isSanitizedJourneyEvent, sanitizeJourneyEvent } from '../core/sanitize';
2
2
  import { getJourneyRumFallback, setJourneyRumFallback } from '../global';
3
+ import { journeyLogsLink, requestLogsUrl } from './kibana';
3
4
  export { normalizeTagValue } from '../core/sanitize';
4
5
  const JOURNEY_ACTION = 'user_journey';
5
6
  const DD_RUM_KEY = 'DD_RUM';
@@ -28,6 +29,7 @@ function resolveRum() {
28
29
  return (_getDdRum = getDdRum()) !== null && _getDdRum !== void 0 ? _getDdRum : getJourneyRumFallback();
29
30
  }
30
31
  function toDatadogStep(step) {
32
+ var _step_requests;
31
33
  /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */ return {
32
34
  name: step.name,
33
35
  start_ms: step.startMs,
@@ -39,17 +41,34 @@ function toDatadogStep(step) {
39
41
  ...step.httpStatus !== undefined ? {
40
42
  http_status: step.httpStatus
41
43
  } : {},
44
+ ...((_step_requests = step.requests) === null || _step_requests === void 0 ? void 0 : _step_requests.length) ? {
45
+ requests: step.requests.map((request)=>({
46
+ request_id: request.rayId,
47
+ started_at: new Date(request.startedAtEpoch).toISOString(),
48
+ duration_ms: request.durationMs,
49
+ http_status: request.status,
50
+ kibana_url: requestLogsUrl(request)
51
+ }))
52
+ } : {},
53
+ ...step.requestsTruncated ? {
54
+ requests_truncated: true
55
+ } : {},
42
56
  ...step.attributes && Object.keys(step.attributes).length ? {
43
57
  attributes: step.attributes
44
58
  } : {}
45
59
  };
46
60
  /* eslint-enable @typescript-eslint/naming-convention */ }
47
61
  /** Ship one terminal journey as a RUM custom action (`user_journey`). */ export function sendToDatadog(payload) {
62
+ var _globalThis_location;
48
63
  const rum = resolveRum();
49
64
  if (!rum || typeof rum.addAction !== 'function') {
50
65
  return;
51
66
  }
52
67
  const j = isSanitizedJourneyEvent(payload) ? payload.journey : sanitizeJourneyEvent(payload).journey;
68
+ const logs = journeyLogsLink(j, ()=>{
69
+ var _rum_getUser;
70
+ return (_rum_getUser = rum.getUser) === null || _rum_getUser === void 0 ? void 0 : _rum_getUser.call(rum);
71
+ }, (_globalThis_location = globalThis.location) === null || _globalThis_location === void 0 ? void 0 : _globalThis_location.href);
53
72
  /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */ rum.addAction(JOURNEY_ACTION, {
54
73
  journey: {
55
74
  name: j.name,
@@ -57,6 +76,13 @@ function toDatadogStep(step) {
57
76
  group: j.group,
58
77
  service: j.service,
59
78
  outcome: j.outcome,
79
+ ...j.failedRequest ? {
80
+ failed_request_id: j.failedRequest.rayId
81
+ } : {},
82
+ ...logs ? {
83
+ kibana_url: logs.url,
84
+ kibana_scope: logs.scope
85
+ } : {},
60
86
  ...j.reason ? {
61
87
  reason: j.reason
62
88
  } : {},
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sinks/datadog.ts"],"sourcesContent":["import { isSanitizedJourneyEvent, sanitizeJourneyEvent } from '../core/sanitize';\nimport { getJourneyRumFallback, setJourneyRumFallback } from '../global';\nimport type { JourneyEvent, JourneyStepEvent } from '../core/types';\n\nexport { normalizeTagValue } from '../core/sanitize';\n\nconst JOURNEY_ACTION = 'user_journey';\n\nconst DD_RUM_KEY = 'DD_RUM';\n\n/**\n * Not imported from @datadog/browser-rum on purpose: importing it sets globalThis.DD_RUM at\n * module load, which lets an MFE bundle replace the host's initialized RUM instance.\n */\ninterface RumLike {\n addAction(name: string, context?: object): void;\n}\n\nfunction getDdRum(): RumLike | undefined {\n return (globalThis as Record<string, unknown>)[DD_RUM_KEY] as RumLike | undefined;\n}\n\n/**\n * Publish a RUM instance for MFE bundles to share — **only needed if\n * `globalThis.DD_RUM` is not set**.\n *\n * Prefer exposing the initialized instance as `globalThis.DD_RUM` (ST host:\n * `datadogGuard`). `sendToDatadog` resolves `DD_RUM` first, then `__stJourney.rum`\n * (this fallback); with neither present the event is dropped.\n *\n * Calling `setJourneyRum` while `globalThis.DD_RUM` is already set logs a warning —\n * the call is redundant because `sendToDatadog` prefers `DD_RUM`.\n */\nexport function setJourneyRum(rum: unknown): void {\n if (getDdRum()) {\n // eslint-disable-next-line no-console -- soft-deprecate redundant bridge use\n console.warn(\n '[journey] setJourneyRum() is unnecessary when globalThis.DD_RUM is set — sendToDatadog already prefers DD_RUM'\n );\n }\n setJourneyRumFallback(rum);\n}\n\nfunction resolveRum(): RumLike | undefined {\n return getDdRum() ?? (getJourneyRumFallback() as RumLike | undefined);\n}\n\nfunction toDatadogStep(step: JourneyStepEvent) {\n /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */\n return {\n name: step.name,\n start_ms: step.startMs,\n duration_ms: step.durationMs,\n outcome: step.outcome,\n ...(step.reason ? { reason: step.reason } : {}),\n ...(step.httpStatus !== undefined ? { http_status: step.httpStatus } : {}),\n ...(step.attributes && Object.keys(step.attributes).length\n ? { attributes: step.attributes }\n : {}),\n };\n /* eslint-enable @typescript-eslint/naming-convention */\n}\n\n/** Ship one terminal journey as a RUM custom action (`user_journey`). */\nexport function sendToDatadog(payload: JourneyEvent): void {\n const rum = resolveRum();\n if (!rum || typeof rum.addAction !== 'function') {\n return;\n }\n\n const j = isSanitizedJourneyEvent(payload)\n ? payload.journey\n : sanitizeJourneyEvent(payload).journey;\n /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */\n rum.addAction(JOURNEY_ACTION, {\n journey: {\n name: j.name,\n team: j.team,\n group: j.group,\n service: j.service,\n outcome: j.outcome,\n ...(j.reason ? { reason: j.reason } : {}),\n duration_ms: j.durationMs,\n steps: j.steps.map(toDatadogStep),\n ...(j.expected ? { expected: j.expected } : {}),\n ...(j.tags ? { tags: j.tags } : {}),\n },\n });\n /* eslint-enable @typescript-eslint/naming-convention */\n}\n"],"names":["isSanitizedJourneyEvent","sanitizeJourneyEvent","getJourneyRumFallback","setJourneyRumFallback","normalizeTagValue","JOURNEY_ACTION","DD_RUM_KEY","getDdRum","globalThis","setJourneyRum","rum","console","warn","resolveRum","toDatadogStep","step","name","start_ms","startMs","duration_ms","durationMs","outcome","reason","httpStatus","undefined","http_status","attributes","Object","keys","length","sendToDatadog","payload","addAction","j","journey","team","group","service","steps","map","expected","tags"],"mappings":"AAAA,SAASA,uBAAuB,EAAEC,oBAAoB,QAAQ,mBAAmB;AACjF,SAASC,qBAAqB,EAAEC,qBAAqB,QAAQ,YAAY;AAGzE,SAASC,iBAAiB,QAAQ,mBAAmB;AAErD,MAAMC,iBAAiB;AAEvB,MAAMC,aAAa;AAUnB,SAASC;IACL,OAAO,AAACC,UAAsC,CAACF,WAAW;AAC9D;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASG,cAAcC,GAAY;IACtC,IAAIH,YAAY;QACZ,6EAA6E;QAC7EI,QAAQC,IAAI,CACR;IAER;IACAT,sBAAsBO;AAC1B;AAEA,SAASG;QACEN;IAAP,QAAOA,YAAAA,wBAAAA,uBAAAA,YAAeL;AAC1B;AAEA,SAASY,cAAcC,IAAsB;IACzC,+FAA+F,GAC/F,OAAO;QACHC,MAAMD,KAAKC,IAAI;QACfC,UAAUF,KAAKG,OAAO;QACtBC,aAAaJ,KAAKK,UAAU;QAC5BC,SAASN,KAAKM,OAAO;QACrB,GAAIN,KAAKO,MAAM,GAAG;YAAEA,QAAQP,KAAKO,MAAM;QAAC,IAAI,CAAC,CAAC;QAC9C,GAAIP,KAAKQ,UAAU,KAAKC,YAAY;YAAEC,aAAaV,KAAKQ,UAAU;QAAC,IAAI,CAAC,CAAC;QACzE,GAAIR,KAAKW,UAAU,IAAIC,OAAOC,IAAI,CAACb,KAAKW,UAAU,EAAEG,MAAM,GACpD;YAAEH,YAAYX,KAAKW,UAAU;QAAC,IAC9B,CAAC,CAAC;IACZ;AACA,sDAAsD,GAC1D;AAEA,uEAAuE,GACvE,OAAO,SAASI,cAAcC,OAAqB;IAC/C,MAAMrB,MAAMG;IACZ,IAAI,CAACH,OAAO,OAAOA,IAAIsB,SAAS,KAAK,YAAY;QAC7C;IACJ;IAEA,MAAMC,IAAIjC,wBAAwB+B,WAC5BA,QAAQG,OAAO,GACfjC,qBAAqB8B,SAASG,OAAO;IAC3C,+FAA+F,GAC/FxB,IAAIsB,SAAS,CAAC3B,gBAAgB;QAC1B6B,SAAS;YACLlB,MAAMiB,EAAEjB,IAAI;YACZmB,MAAMF,EAAEE,IAAI;YACZC,OAAOH,EAAEG,KAAK;YACdC,SAASJ,EAAEI,OAAO;YAClBhB,SAASY,EAAEZ,OAAO;YAClB,GAAIY,EAAEX,MAAM,GAAG;gBAAEA,QAAQW,EAAEX,MAAM;YAAC,IAAI,CAAC,CAAC;YACxCH,aAAac,EAAEb,UAAU;YACzBkB,OAAOL,EAAEK,KAAK,CAACC,GAAG,CAACzB;YACnB,GAAImB,EAAEO,QAAQ,GAAG;gBAAEA,UAAUP,EAAEO,QAAQ;YAAC,IAAI,CAAC,CAAC;YAC9C,GAAIP,EAAEQ,IAAI,GAAG;gBAAEA,MAAMR,EAAEQ,IAAI;YAAC,IAAI,CAAC,CAAC;QACtC;IACJ;AACA,sDAAsD,GAC1D"}
1
+ {"version":3,"sources":["../../src/sinks/datadog.ts"],"sourcesContent":["import { isSanitizedJourneyEvent, sanitizeJourneyEvent } from '../core/sanitize';\nimport { getJourneyRumFallback, setJourneyRumFallback } from '../global';\nimport type { JourneyEvent, JourneyStepEvent } from '../core/types';\nimport { journeyLogsLink, requestLogsUrl } from './kibana';\n\nexport { normalizeTagValue } from '../core/sanitize';\n\nconst JOURNEY_ACTION = 'user_journey';\n\nconst DD_RUM_KEY = 'DD_RUM';\n\n/**\n * Not imported from @datadog/browser-rum on purpose: importing it sets globalThis.DD_RUM at\n * module load, which lets an MFE bundle replace the host's initialized RUM instance.\n */\ninterface RumLike {\n addAction(name: string, context?: object): void;\n getUser?(): unknown;\n}\n\nfunction getDdRum(): RumLike | undefined {\n return (globalThis as Record<string, unknown>)[DD_RUM_KEY] as RumLike | undefined;\n}\n\n/**\n * Publish a RUM instance for MFE bundles to share — **only needed if\n * `globalThis.DD_RUM` is not set**.\n *\n * Prefer exposing the initialized instance as `globalThis.DD_RUM` (ST host:\n * `datadogGuard`). `sendToDatadog` resolves `DD_RUM` first, then `__stJourney.rum`\n * (this fallback); with neither present the event is dropped.\n *\n * Calling `setJourneyRum` while `globalThis.DD_RUM` is already set logs a warning —\n * the call is redundant because `sendToDatadog` prefers `DD_RUM`.\n */\nexport function setJourneyRum(rum: unknown): void {\n if (getDdRum()) {\n // eslint-disable-next-line no-console -- soft-deprecate redundant bridge use\n console.warn(\n '[journey] setJourneyRum() is unnecessary when globalThis.DD_RUM is set — sendToDatadog already prefers DD_RUM'\n );\n }\n setJourneyRumFallback(rum);\n}\n\nfunction resolveRum(): RumLike | undefined {\n return getDdRum() ?? (getJourneyRumFallback() as RumLike | undefined);\n}\n\nfunction toDatadogStep(step: JourneyStepEvent) {\n /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */\n return {\n name: step.name,\n start_ms: step.startMs,\n duration_ms: step.durationMs,\n outcome: step.outcome,\n ...(step.reason ? { reason: step.reason } : {}),\n ...(step.httpStatus !== undefined ? { http_status: step.httpStatus } : {}),\n ...(step.requests?.length\n ? {\n requests: step.requests.map(request => ({\n request_id: request.rayId,\n started_at: new Date(request.startedAtEpoch).toISOString(),\n duration_ms: request.durationMs,\n http_status: request.status,\n kibana_url: requestLogsUrl(request),\n })),\n }\n : {}),\n ...(step.requestsTruncated ? { requests_truncated: true } : {}),\n ...(step.attributes && Object.keys(step.attributes).length\n ? { attributes: step.attributes }\n : {}),\n };\n /* eslint-enable @typescript-eslint/naming-convention */\n}\n\n/** Ship one terminal journey as a RUM custom action (`user_journey`). */\nexport function sendToDatadog(payload: JourneyEvent): void {\n const rum = resolveRum();\n if (!rum || typeof rum.addAction !== 'function') {\n return;\n }\n\n const j = isSanitizedJourneyEvent(payload)\n ? payload.journey\n : sanitizeJourneyEvent(payload).journey;\n const logs = journeyLogsLink(j, () => rum.getUser?.(), globalThis.location?.href);\n /* eslint-disable @typescript-eslint/naming-convention -- Datadog RUM wire format (snake_case) */\n rum.addAction(JOURNEY_ACTION, {\n journey: {\n name: j.name,\n team: j.team,\n group: j.group,\n service: j.service,\n outcome: j.outcome,\n ...(j.failedRequest\n ? {\n failed_request_id: j.failedRequest.rayId,\n }\n : {}),\n ...(logs ? { kibana_url: logs.url, kibana_scope: logs.scope } : {}),\n ...(j.reason ? { reason: j.reason } : {}),\n duration_ms: j.durationMs,\n steps: j.steps.map(toDatadogStep),\n ...(j.expected ? { expected: j.expected } : {}),\n ...(j.tags ? { tags: j.tags } : {}),\n },\n });\n /* eslint-enable @typescript-eslint/naming-convention */\n}\n"],"names":["isSanitizedJourneyEvent","sanitizeJourneyEvent","getJourneyRumFallback","setJourneyRumFallback","journeyLogsLink","requestLogsUrl","normalizeTagValue","JOURNEY_ACTION","DD_RUM_KEY","getDdRum","globalThis","setJourneyRum","rum","console","warn","resolveRum","toDatadogStep","step","name","start_ms","startMs","duration_ms","durationMs","outcome","reason","httpStatus","undefined","http_status","requests","length","map","request","request_id","rayId","started_at","Date","startedAtEpoch","toISOString","status","kibana_url","requestsTruncated","requests_truncated","attributes","Object","keys","sendToDatadog","payload","addAction","j","journey","logs","getUser","location","href","team","group","service","failedRequest","failed_request_id","url","kibana_scope","scope","steps","expected","tags"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { JourneyEvent, JourneyRequestEvent } from '../core/types';
2
+ /** Fixed destination and validated Ray ID only — no request URL, query string or user data. */
3
+ export declare function requestLogsUrl(value: JourneyRequestEvent): string | undefined;
4
+ interface JourneyLogLink {
5
+ url: string;
6
+ scope: 'request' | 'related_requests' | 'related_logs';
7
+ }
8
+ /** Keep one visible link without claiming unrelated requests caused a JS/business failure. */
9
+ export declare function journeyLogsLink(journey: JourneyEvent['journey'], getUser?: () => unknown, pageUrl?: string): JourneyLogLink | undefined;
10
+ export {};
11
+ //# sourceMappingURL=kibana.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kibana.d.ts","sourceRoot":"","sources":["../../src/sinks/kibana.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAOvE,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAY7E;AAED,UAAU,cAAc;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,kBAAkB,GAAG,cAAc,CAAC;CAC1D;AA4DD,8FAA8F;AAC9F,wBAAgB,eAAe,CAC3B,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,OAAO,CAAC,EAAE,MAAM,OAAO,EACvB,OAAO,CAAC,EAAE,MAAM,GACjB,cAAc,GAAG,SAAS,CA4C5B"}
@@ -0,0 +1,92 @@
1
+ import { isMonolithLogOrigin, MAX_JOURNEY_CORRELATED_REQUESTS, sanitizeRequestCorrelation } from '../core/request-correlation';
2
+ const DISCOVER_URL = 'https://kibana.st.dev/app/discover#/';
3
+ // Production monolith logs; deliberately not the user's default data view.
4
+ const DATA_VIEW_ID = '894110ed-c22d-449c-9dc2-c4816f08712a';
5
+ const WINDOW_PADDING_MS = 60000;
6
+ /** Fixed destination and validated Ray ID only — no request URL, query string or user data. */ export function requestLogsUrl(value) {
7
+ const request = sanitizeRequestCorrelation(value);
8
+ if (!request) {
9
+ return undefined;
10
+ }
11
+ const from = new Date(request.startedAtEpoch - WINDOW_PADDING_MS).toISOString();
12
+ const to = new Date(request.startedAtEpoch + request.durationMs + WINDOW_PADDING_MS).toISOString();
13
+ // Origin/edge datacenter suffixes can differ. Match the hex prefix within the request window.
14
+ const rayPrefix = request.rayId.slice(0, 16);
15
+ return discoverUrl(`Request.RequestId: ${rayPrefix}*`, from, to);
16
+ }
17
+ function discoverUrl(query, from, to) {
18
+ const globalState = `(refreshInterval:(pause:!t,value:0),time:(from:'${from}',to:'${to}'))`;
19
+ // The query is a quoted Rison string containing a separately escaped KQL expression.
20
+ const quotedQuery = query.replace(/!/g, '!!').replace(/'/g, "!'");
21
+ const appState = `(dataSource:(type:dataView,dataViewId:'${DATA_VIEW_ID}'),query:(language:kuery,query:'${quotedQuery}'))`;
22
+ return `${DISCOVER_URL}?_g=${encodeURIComponent(globalState)}&_a=${encodeURIComponent(appState)}`;
23
+ }
24
+ function userValue(value) {
25
+ return typeof value === 'string' && value.length > 0 && value.length <= 254 && // eslint-disable-next-line no-control-regex -- Reject controls in log query values.
26
+ !/[\u0000-\u001f\u007f]/.test(value) ? value : undefined;
27
+ }
28
+ function kqlValue(value) {
29
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
30
+ }
31
+ function relatedLogsUrl(durationMs, user, pageUrl) {
32
+ if (!pageUrl || !isMonolithLogOrigin(pageUrl) || !user || typeof user !== 'object' || !Number.isFinite(durationMs) || durationMs < 0 || durationMs > 86400000) {
33
+ return;
34
+ }
35
+ const context = user;
36
+ const tenant = userValue(context.tenant);
37
+ const users = [
38
+ ...new Set([
39
+ userValue(context.name),
40
+ userValue(context.email)
41
+ ])
42
+ ].filter((value)=>value !== undefined);
43
+ // Never fall back to all tenants or all users when identity is missing.
44
+ if (!tenant || !/^[a-z0-9][a-z0-9_-]*$/i.test(tenant) || users.length === 0) {
45
+ return;
46
+ }
47
+ const query = `Context.TenantName: ${kqlValue(tenant)} AND (${users.map((value)=>`Request.User: ${kqlValue(value)}`).join(' OR ')})`;
48
+ const now = Date.now();
49
+ return discoverUrl(query, new Date(now - durationMs - WINDOW_PADDING_MS).toISOString(), new Date(now + WINDOW_PADDING_MS).toISOString());
50
+ }
51
+ /** Keep one visible link without claiming unrelated requests caused a JS/business failure. */ export function journeyLogsLink(journey, getUser, pageUrl) {
52
+ if (journey.failedRequest) {
53
+ const url = requestLogsUrl(journey.failedRequest);
54
+ if (url) {
55
+ return {
56
+ url,
57
+ scope: 'request'
58
+ };
59
+ }
60
+ }
61
+ /*
62
+ * With no identified failure request, search this user's logs during the journey.
63
+ * RUM stubs/older hosts can lack getUser; enrichment must never prevent emission.
64
+ */ try {
65
+ const url = relatedLogsUrl(journey.durationMs, getUser === null || getUser === void 0 ? void 0 : getUser(), pageUrl);
66
+ if (url) {
67
+ return {
68
+ url,
69
+ scope: 'related_logs'
70
+ };
71
+ }
72
+ } catch (unused) {
73
+ // Retain request-only links if session context is unavailable or malformed.
74
+ }
75
+ const requests = journey.steps.flatMap((step)=>{
76
+ var _step_requests;
77
+ return (_step_requests = step.requests) !== null && _step_requests !== void 0 ? _step_requests : [];
78
+ }).slice(0, MAX_JOURNEY_CORRELATED_REQUESTS).map(sanitizeRequestCorrelation).filter((request)=>request !== undefined);
79
+ if (!requests.length) {
80
+ return;
81
+ }
82
+ const prefixes = [
83
+ ...new Set(requests.map((request)=>request.rayId.slice(0, 16)))
84
+ ];
85
+ const query = prefixes.map((prefix)=>`Request.RequestId: ${prefix}*`).join(' OR ');
86
+ return {
87
+ url: discoverUrl(query, new Date(Math.min(...requests.map((request)=>request.startedAtEpoch)) - WINDOW_PADDING_MS).toISOString(), new Date(Math.max(...requests.map((request)=>request.startedAtEpoch + request.durationMs)) + WINDOW_PADDING_MS).toISOString()),
88
+ scope: 'related_requests'
89
+ };
90
+ }
91
+
92
+ //# sourceMappingURL=kibana.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sinks/kibana.ts"],"sourcesContent":["import {\n isMonolithLogOrigin,\n MAX_JOURNEY_CORRELATED_REQUESTS,\n sanitizeRequestCorrelation,\n} from '../core/request-correlation';\nimport type { JourneyEvent, JourneyRequestEvent } from '../core/types';\n\nconst DISCOVER_URL = 'https://kibana.st.dev/app/discover#/';\n// Production monolith logs; deliberately not the user's default data view.\nconst DATA_VIEW_ID = '894110ed-c22d-449c-9dc2-c4816f08712a';\nconst WINDOW_PADDING_MS = 60_000;\n\n/** Fixed destination and validated Ray ID only — no request URL, query string or user data. */\nexport function requestLogsUrl(value: JourneyRequestEvent): string | undefined {\n const request = sanitizeRequestCorrelation(value);\n if (!request) {\n return undefined;\n }\n const from = new Date(request.startedAtEpoch - WINDOW_PADDING_MS).toISOString();\n const to = new Date(\n request.startedAtEpoch + request.durationMs + WINDOW_PADDING_MS\n ).toISOString();\n // Origin/edge datacenter suffixes can differ. Match the hex prefix within the request window.\n const rayPrefix = request.rayId.slice(0, 16);\n return discoverUrl(`Request.RequestId: ${rayPrefix}*`, from, to);\n}\n\ninterface JourneyLogLink {\n url: string;\n scope: 'request' | 'related_requests' | 'related_logs';\n}\n\nfunction discoverUrl(query: string, from: string, to: string): string {\n const globalState = `(refreshInterval:(pause:!t,value:0),time:(from:'${from}',to:'${to}'))`;\n // The query is a quoted Rison string containing a separately escaped KQL expression.\n const quotedQuery = query.replace(/!/g, '!!').replace(/'/g, \"!'\");\n const appState = `(dataSource:(type:dataView,dataViewId:'${DATA_VIEW_ID}'),query:(language:kuery,query:'${quotedQuery}'))`;\n return `${DISCOVER_URL}?_g=${encodeURIComponent(globalState)}&_a=${encodeURIComponent(appState)}`;\n}\n\nfunction userValue(value: unknown): string | undefined {\n return typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 254 &&\n // eslint-disable-next-line no-control-regex -- Reject controls in log query values.\n !/[\\u0000-\\u001f\\u007f]/.test(value)\n ? value\n : undefined;\n}\n\nfunction kqlValue(value: string): string {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\nfunction relatedLogsUrl(\n durationMs: number,\n user: unknown,\n pageUrl: string | undefined\n): string | undefined {\n if (\n !pageUrl ||\n !isMonolithLogOrigin(pageUrl) ||\n !user ||\n typeof user !== 'object' ||\n !Number.isFinite(durationMs) ||\n durationMs < 0 ||\n durationMs > 86_400_000\n ) {\n return;\n }\n const context = user as Record<string, unknown>;\n const tenant = userValue(context.tenant);\n const users = [...new Set([userValue(context.name), userValue(context.email)])].filter(\n (value): value is string => value !== undefined\n );\n // Never fall back to all tenants or all users when identity is missing.\n if (!tenant || !/^[a-z0-9][a-z0-9_-]*$/i.test(tenant) || users.length === 0) {\n return;\n }\n const query = `Context.TenantName: ${kqlValue(tenant)} AND (${users\n .map(value => `Request.User: ${kqlValue(value)}`)\n .join(' OR ')})`;\n const now = Date.now();\n return discoverUrl(\n query,\n new Date(now - durationMs - WINDOW_PADDING_MS).toISOString(),\n new Date(now + WINDOW_PADDING_MS).toISOString()\n );\n}\n\n/** Keep one visible link without claiming unrelated requests caused a JS/business failure. */\nexport function journeyLogsLink(\n journey: JourneyEvent['journey'],\n getUser?: () => unknown,\n pageUrl?: string\n): JourneyLogLink | undefined {\n if (journey.failedRequest) {\n const url = requestLogsUrl(journey.failedRequest);\n if (url) {\n return { url, scope: 'request' };\n }\n }\n\n /*\n * With no identified failure request, search this user's logs during the journey.\n * RUM stubs/older hosts can lack getUser; enrichment must never prevent emission.\n */\n try {\n const url = relatedLogsUrl(journey.durationMs, getUser?.(), pageUrl);\n if (url) {\n return { url, scope: 'related_logs' };\n }\n } catch {\n // Retain request-only links if session context is unavailable or malformed.\n }\n\n const requests = journey.steps\n .flatMap(step => step.requests ?? [])\n .slice(0, MAX_JOURNEY_CORRELATED_REQUESTS)\n .map(sanitizeRequestCorrelation)\n .filter((request): request is JourneyRequestEvent => request !== undefined);\n if (!requests.length) {\n return;\n }\n const prefixes = [...new Set(requests.map(request => request.rayId.slice(0, 16)))];\n const query = prefixes.map(prefix => `Request.RequestId: ${prefix}*`).join(' OR ');\n return {\n url: discoverUrl(\n query,\n new Date(\n Math.min(...requests.map(request => request.startedAtEpoch)) - WINDOW_PADDING_MS\n ).toISOString(),\n new Date(\n Math.max(...requests.map(request => request.startedAtEpoch + request.durationMs)) +\n WINDOW_PADDING_MS\n ).toISOString()\n ),\n scope: 'related_requests',\n };\n}\n"],"names":["isMonolithLogOrigin","MAX_JOURNEY_CORRELATED_REQUESTS","sanitizeRequestCorrelation","DISCOVER_URL","DATA_VIEW_ID","WINDOW_PADDING_MS","requestLogsUrl","value","request","undefined","from","Date","startedAtEpoch","toISOString","to","durationMs","rayPrefix","rayId","slice","discoverUrl","query","globalState","quotedQuery","replace","appState","encodeURIComponent","userValue","length","test","kqlValue","relatedLogsUrl","user","pageUrl","Number","isFinite","context","tenant","users","Set","name","email","filter","map","join","now","journeyLogsLink","journey","getUser","failedRequest","url","scope","requests","steps","flatMap","step","prefixes","prefix","Math","min","max"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/journey",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Critical user journey monitoring — reports good/bad/excluded verdicts to Datadog RUM as a custom action",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/journey",
6
6
  "repository": {
@@ -39,5 +39,5 @@
39
39
  "cli": {
40
40
  "webpack": false
41
41
  },
42
- "gitHead": "cd7d26dcbb74336efe088749dd930cf31715beed"
42
+ "gitHead": "dcfd73b9e16731d66271c95b6fa37ef638ae8641"
43
43
  }
@@ -1,5 +1,5 @@
1
1
  /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
2
+ import { baseJourneyDef, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
3
  import { completeJourney, journeyStep } from '../../core';
4
4
  import { moduleRuntime } from '../../core/runtime';
5
5
  import { getSharedPolicies } from '../../global';
@@ -614,3 +614,40 @@ describe('[journey] instrumentAxios', () => {
614
614
  expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
615
615
  });
616
616
  });
617
+
618
+ describe('[journey] Axios response correlation', () => {
619
+ useJourneyTestRuntime(emitMock);
620
+ test.each(['plain', 'get'])(
621
+ 'captures %s headers and preserves the rejection identity',
622
+ async shape => {
623
+ const axios = createAxiosMock();
624
+ axios.defaults.baseURL = 'https://go.servicetitan.com';
625
+ const restore = instrumentAxios(axios);
626
+ const headers =
627
+ shape === 'plain'
628
+ ? { 'cf-ray': '0123456789abcdef-IAD' }
629
+ : { get: jest.fn().mockReturnValue('0123456789abcdef-IAD') };
630
+ let error: Error;
631
+ try {
632
+ await expect(
633
+ journeyStep({ ...baseJourneyDef, name: 'axios-ray' }, 'save', async step => {
634
+ const config = axios.requestHandlers[0]({
635
+ url: '/test',
636
+ stamp: step.stamp(),
637
+ });
638
+ error = Object.assign(new Error('server error'), {
639
+ config,
640
+ response: { config, status: 500, headers },
641
+ });
642
+ await axios.responseErrHandlers[0](error);
643
+ })
644
+ ).rejects.toBe(error!);
645
+ const event = emitMock.mock.calls[0][0].journey;
646
+ expect(event.reason).toBe('request-error');
647
+ expect(event.steps[0].requests[0].rayId).toBe('0123456789abcdef-iad');
648
+ } finally {
649
+ restore();
650
+ }
651
+ }
652
+ );
653
+ });
@@ -1,5 +1,5 @@
1
1
  /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
2
+ import { baseJourneyDef, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
3
  import { completeJourney, journeyStep } from '../../core';
4
4
  import { moduleRuntime } from '../../core/runtime';
5
5
  import { getSharedPolicies } from '../../global';
@@ -537,3 +537,96 @@ describe('[journey] instrumentJquery', () => {
537
537
  expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
538
538
  });
539
539
  });
540
+
541
+ describe('[journey] jQuery early response correlation', () => {
542
+ useJourneyTestRuntime(emitMock);
543
+
544
+ function pendingAjax($: ReturnType<typeof createJqueryMock>, stamp: unknown) {
545
+ const callbacks: (() => void)[] = [];
546
+ const xhr = {
547
+ status: 500,
548
+ getResponseHeader: jest.fn().mockReturnValue('0123456789abcdef-IAD'),
549
+ always(callback: () => void) {
550
+ callbacks.push(callback);
551
+ },
552
+ };
553
+ const settings = { url: 'https://go.servicetitan.com/test', stamp };
554
+ for (const prefilter of $.__prefilters) {
555
+ prefilter(settings, settings, xhr);
556
+ }
557
+ return { xhr, settings, callbacks };
558
+ }
559
+
560
+ test('captures before a synchronous application failure and does not duplicate at ajaxComplete', async () => {
561
+ const $ = createJqueryMock();
562
+ const restore = instrumentJquery($);
563
+ try {
564
+ await journeyStep({ ...baseJourneyDef, name: 'jq-early' }, 'save', step => {
565
+ const { xhr, settings, callbacks } = pendingAjax($, step.stamp());
566
+ callbacks.forEach(callback => callback());
567
+ expect(emitMock).not.toHaveBeenCalled();
568
+ step.failJourney('application-error');
569
+ for (const complete of $.__completes) {
570
+ complete({}, xhr, settings);
571
+ }
572
+ });
573
+ const event = emitMock.mock.calls[0][0].journey;
574
+ expect(emitMock).toHaveBeenCalledTimes(1);
575
+ expect(event.reason).toBe('application-error');
576
+ expect(event.steps[0].requests).toHaveLength(1);
577
+ expect(event.steps[0].requests[0].rayId).toBe('0123456789abcdef-iad');
578
+ } finally {
579
+ restore();
580
+ }
581
+ });
582
+
583
+ test('keeps scoring after application callbacks, as before', async () => {
584
+ const $ = createJqueryMock();
585
+ const restore = instrumentJquery($);
586
+ try {
587
+ await journeyStep({ ...baseJourneyDef, name: 'jq-scoring' }, 'save', step => {
588
+ const { xhr, settings, callbacks } = pendingAjax($, step.stamp());
589
+ callbacks.forEach(callback => callback());
590
+ expect(emitMock).not.toHaveBeenCalled();
591
+ for (const complete of $.__completes) {
592
+ complete({}, xhr, settings);
593
+ }
594
+ });
595
+ const event = emitMock.mock.calls[0][0].journey;
596
+ expect(event.reason).toBe('request-error');
597
+ expect(event.steps[0].requests).toHaveLength(1);
598
+ } finally {
599
+ restore();
600
+ }
601
+ });
602
+
603
+ test('restore prevents early observers from modifying outstanding requests', async () => {
604
+ const $ = createJqueryMock();
605
+ const restore = instrumentJquery($);
606
+ await journeyStep({ ...baseJourneyDef, name: 'jq-restored' }, 'save', step => {
607
+ const { callbacks } = pendingAjax($, step.stamp());
608
+ restore();
609
+ callbacks.forEach(callback => callback());
610
+ step.failJourney('application-error');
611
+ });
612
+ expect(emitMock.mock.calls[0][0].journey.steps[0].requests).toBeUndefined();
613
+ });
614
+
615
+ test('a throwing header reader cannot stop the application callback', async () => {
616
+ const $ = createJqueryMock();
617
+ const restore = instrumentJquery($);
618
+ try {
619
+ await journeyStep({ ...baseJourneyDef, name: 'jq-blocked' }, 'save', step => {
620
+ const { xhr, callbacks } = pendingAjax($, step.stamp());
621
+ xhr.getResponseHeader.mockImplementation(() => {
622
+ throw new Error('blocked');
623
+ });
624
+ expect(() => callbacks.forEach(callback => callback())).not.toThrow();
625
+ step.failJourney('application-error');
626
+ });
627
+ expect(emitMock.mock.calls[0][0].journey.steps[0].requests).toBeUndefined();
628
+ } finally {
629
+ restore();
630
+ }
631
+ });
632
+ });