@alwaysmeticulous/backend-recorder-workerd 2.325.0 → 2.327.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 (78) hide show
  1. package/README.md +94 -4
  2. package/dist/binding-patch.js +17 -8
  3. package/dist/binding-patch.js.map +1 -1
  4. package/dist/body-capture.js +2 -2
  5. package/dist/body-capture.js.map +1 -1
  6. package/dist/capture-buffer.js +114 -0
  7. package/dist/capture-buffer.js.map +1 -0
  8. package/dist/context.js +6 -3
  9. package/dist/context.js.map +1 -1
  10. package/dist/error-capture.d.ts +44 -0
  11. package/dist/error-capture.js +65 -0
  12. package/dist/error-capture.js.map +1 -0
  13. package/dist/fetch-patch.js +5 -5
  14. package/dist/fetch-patch.js.map +1 -1
  15. package/dist/index.d.ts +62 -39
  16. package/dist/index.js +65 -179
  17. package/dist/index.js.map +1 -1
  18. package/dist/kv-capture.js +2 -2
  19. package/dist/kv-capture.js.map +1 -1
  20. package/dist/kv-patch.js +9 -9
  21. package/dist/kv-patch.js.map +1 -1
  22. package/dist/log.js +2 -2
  23. package/dist/log.js.map +1 -1
  24. package/dist/original-fetch.js +2 -2
  25. package/dist/original-fetch.js.map +1 -1
  26. package/dist/outbound-capture.js +20 -28
  27. package/dist/outbound-capture.js.map +1 -1
  28. package/dist/pages-function.d.ts +33 -0
  29. package/dist/pages-function.js +28 -0
  30. package/dist/pages-function.js.map +1 -0
  31. package/dist/postgres/capture.d.ts +108 -0
  32. package/dist/postgres/capture.js +136 -0
  33. package/dist/postgres/capture.js.map +1 -0
  34. package/dist/postgres/with-meticulous-postgres.d.ts +32 -0
  35. package/dist/postgres/with-meticulous-postgres.js +152 -0
  36. package/dist/postgres/with-meticulous-postgres.js.map +1 -0
  37. package/dist/protocol.d.ts +88 -28
  38. package/dist/protocol.js +15 -10
  39. package/dist/protocol.js.map +1 -1
  40. package/dist/provisional-session-id.d.ts +58 -0
  41. package/dist/provisional-session-id.js +75 -0
  42. package/dist/provisional-session-id.js.map +1 -0
  43. package/dist/publish-session-id.js +41 -0
  44. package/dist/publish-session-id.js.map +1 -0
  45. package/dist/redact-body.js +2 -2
  46. package/dist/redact-body.js.map +1 -1
  47. package/dist/replay-fetch.js +17 -13
  48. package/dist/replay-fetch.js.map +1 -1
  49. package/dist/replay-sidecar-url.js +2 -2
  50. package/dist/replay-sidecar-url.js.map +1 -1
  51. package/dist/sidecar-client.js +31 -20
  52. package/dist/sidecar-client.js.map +1 -1
  53. package/dist/sidecar-transport.d.ts +38 -0
  54. package/dist/sidecar-transport.js +52 -0
  55. package/dist/sidecar-transport.js.map +1 -0
  56. package/dist/spans/attributes.d.ts +84 -0
  57. package/dist/spans/attributes.js +88 -0
  58. package/dist/spans/attributes.js.map +1 -0
  59. package/dist/spans/readable-span.d.ts +78 -0
  60. package/dist/spans/readable-span.js +35 -0
  61. package/dist/spans/readable-span.js.map +1 -0
  62. package/dist/spans/serialize-span.d.ts +14 -0
  63. package/dist/spans/serialize-span.js +38 -0
  64. package/dist/spans/serialize-span.js.map +1 -0
  65. package/dist/spans/span-builder.d.ts +61 -0
  66. package/dist/spans/span-builder.js +250 -0
  67. package/dist/spans/span-builder.js.map +1 -0
  68. package/dist/spans/url-redaction.d.ts +14 -0
  69. package/dist/spans/url-redaction.js +40 -0
  70. package/dist/spans/url-redaction.js.map +1 -0
  71. package/dist/virtual-clock.js +2 -2
  72. package/dist/virtual-clock.js.map +1 -1
  73. package/dist/virtual-random.js +47 -20
  74. package/dist/virtual-random.js.map +1 -1
  75. package/dist/with-meticulous.d.ts +32 -0
  76. package/dist/with-meticulous.js +0 -0
  77. package/dist/with-meticulous.js.map +1 -0
  78. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-client.js","sources":["../src/sidecar-client.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport {\n type CaptureEvent,\n type CaptureEventsPayload,\n type OutboundFetchLookupRequest,\n type OutboundFetchLookupResponse,\n type ReplaySessionInfoResponse,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n/**\n * Replay calls sit in the request path, unlike record mode's fire-and-forget reporting, so\n * they must be bounded: without a timeout a wedged sidecar would hang every outbound fetch\n * the app makes, and therefore the whole replay, with nothing to look at.\n */\nconst SESSION_INFO_TIMEOUT_MS = 10_000;\nconst LOOKUP_TIMEOUT_MS = 5_000;\n\n/**\n * Capture reporting is off the response path (it runs under `ctx.waitUntil`), so a timeout\n * here is not about latency the app can see — it is about not holding the request context\n * open on a sidecar that will never answer. A refused connection rejects immediately, but a\n * sidecar URL pointing at a host that drops packets rather than refusing them (a firewalled\n * container or LAN address instead of loopback) would otherwise leave the POST pending until\n * the runtime tears the context down, on every captured call.\n *\n * Generous relative to what it bounds: the sidecar is always loopback or the docker gateway\n * and answers a single small event in single-digit milliseconds, so this only ever fires on\n * a sidecar that is wedged or unreachable — never on one that is merely busy. Being generous\n * costs nothing because {@link withTimeout} clears the timer the moment the POST settles.\n */\nconst CAPTURE_POST_TIMEOUT_MS = 2_000;\n\n/** POSTs capture events to the sidecar. Never rejects — reporting failures only warn (once). */\nexport const postCaptureEvents = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n events: CaptureEvent[],\n): Promise<void> => {\n try {\n const payload: CaptureEventsPayload = { events };\n await withTimeout(CAPTURE_POST_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(`${sidecarUrl}${SIDECAR_EVENTS_PATH}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n });\n if (!response.ok) {\n warnOnce(\n \"sidecar-rejected\",\n `Meticulous sidecar rejected capture events (HTTP ${response.status}).`,\n );\n }\n await response.arrayBuffer().catch(() => undefined);\n });\n } catch (error) {\n warnOnce(\n \"sidecar-unreachable\",\n \"Could not reach the Meticulous backend recorder sidecar — backend session events are not being recorded.\",\n error,\n );\n }\n};\n\n/**\n * Outcome of the session handshake.\n *\n * `unavailable` and `unreachable` both mean \"do not replay\", but they are kept apart\n * because only the first is a settled answer. The caller caches per isolate, and caching a\n * one-off network blip would disable mocking for the rest of the session — silently sending\n * every later outbound call to the real service.\n */\nexport type ReplaySessionInfoResult =\n /** The sidecar holds mocks for this session. */\n | { outcome: \"found\"; clockAnchorMs: number | undefined }\n /** The sidecar answered and will never serve this session: no mocks, or no such route. */\n | { outcome: \"unavailable\" }\n /** Could not get an answer — timeout, transport error, 5xx. May succeed later. */\n | { outcome: \"unreachable\" };\n\n/**\n * Asks whether the sidecar holds mocks for a session, and at what instant to freeze the\n * clock while serving it.\n *\n * A 404 is how a record-only sidecar answers, so it counts as a settled `unavailable` and\n * the shim degrades to pass-through against an older sidecar rather than failing.\n */\nexport const getReplaySessionInfo = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<ReplaySessionInfoResult> => {\n const url = `${sidecarUrl}${SIDECAR_REPLAY_SESSION_PATH}?sessionId=${encodeURIComponent(\n frontendSessionId,\n )}`;\n try {\n return await withTimeout(SESSION_INFO_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(url, {\n headers: {\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n signal,\n });\n if (!response.ok) {\n await response.arrayBuffer().catch(() => undefined);\n // 4xx is the sidecar telling us something definitive — no such route (a record-only\n // sidecar), or a protocol version it will not speak. Retrying cannot change either.\n // 5xx may well be a sidecar still finding its feet, so leave it retryable.\n if (response.status >= 500) {\n warnOnce(\n \"replay-session-unreachable\",\n `Meticulous replay sidecar failed the replay session handshake (HTTP ${response.status}) — will retry on the next request.`,\n );\n return { outcome: \"unreachable\" };\n }\n warnOnce(\n \"replay-session-unavailable\",\n `Meticulous replay sidecar did not recognise the replay session route (HTTP ${response.status}) — outbound calls will not be mocked.`,\n );\n return { outcome: \"unavailable\" };\n }\n const info = (await response.json()) as ReplaySessionInfoResponse;\n if (info.found !== true) {\n warnOnce(\n \"replay-session-unknown\",\n \"Meticulous replay sidecar holds no mocks for this session — outbound calls will not be mocked.\",\n );\n return { outcome: \"unavailable\" };\n }\n return {\n outcome: \"found\",\n clockAnchorMs:\n typeof info.clockAnchorMs === \"number\"\n ? info.clockAnchorMs\n : undefined,\n };\n });\n } catch (error) {\n warnOnce(\n \"replay-session-unreachable\",\n \"Could not reach the Meticulous replay sidecar — will retry on the next request.\",\n error,\n );\n return { outcome: \"unreachable\" };\n }\n};\n\n/**\n * Looks up a recorded response for one outbound call. Never rejects; returns undefined when\n * the sidecar could not be consulted, which the caller fails the request on — an unanswered\n * lookup is not evidence that a real call is safe.\n */\nexport const postOutboundFetchLookup = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n payload: OutboundFetchLookupRequest,\n): Promise<OutboundFetchLookupResponse | undefined> => {\n try {\n return await withTimeout(LOOKUP_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(\n `${sidecarUrl}${SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"replay-lookup-rejected\",\n `Meticulous replay sidecar rejected a mock lookup (HTTP ${response.status}).`,\n );\n await response.arrayBuffer().catch(() => undefined);\n return undefined;\n }\n return (await response.json()) as OutboundFetchLookupResponse;\n });\n } catch (error) {\n warnOnce(\n \"replay-lookup-unreachable\",\n \"Could not reach the Meticulous replay sidecar for a mock lookup — the call will fail.\",\n error,\n );\n return undefined;\n }\n};\n\n/**\n * Runs `send` under an abort signal that fires after `ms`, clearing the timer as soon as the\n * call settles — including the response body read, which an abort must still cut short.\n *\n * Deliberately not `AbortSignal.timeout`: its timer cannot be cancelled, and a pending timer\n * keeps workerd's request context alive. Bounding a 5ms POST at 2s would then pin the context\n * for the remaining ~2s on every captured call — worse than the unbounded wait it exists to\n * prevent, and on the healthy path rather than the broken one. `body-capture.ts` uses this\n * same clearable shape, for the same reason.\n */\nconst withTimeout = async <T>(\n ms: number,\n send: (signal: AbortSignal) => Promise<T>,\n): Promise<T> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n try {\n return await send(controller.signal);\n } finally {\n clearTimeout(timer);\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;AAqBA,MAAM,0BAA0B;AAChC,MAAM,oBAAoB;;;;;;;;;;;;;;AAe1B,MAAM,0BAA0B;;AAGhC,MAAa,oBAAoB,OAC/B,SACA,YACA,WACkB;AAClB,KAAI;EACF,MAAM,UAAgC,EAAE,QAAQ;AAChD,QAAM,YAAY,yBAAyB,OAAO,WAAW;GAC3D,MAAM,WAAW,MAAM,QAAQ,GAAG,aAAa,uBAAuB;IACpE,QAAQ;IACR,SAAS;KACP,gBAAgB;MACf,kCAAA;KACF;IACD,MAAM,KAAK,UAAU,QAAQ;IAC7B;IACD,CAAC;AACF,OAAI,CAAC,SAAS,GACZ,UACE,oBACA,oDAAoD,SAAS,OAAO,IACrE;AAEH,SAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;IACnD;UACK,OAAO;AACd,WACE,uBACA,4GACA,MACD;;;;;;;;;;AA2BL,MAAa,uBAAuB,OAClC,SACA,YACA,sBACqC;CACrC,MAAM,MAAM,GAAG,aAAa,4BAA4B,aAAa,mBACnE,kBACD;AACD,KAAI;AACF,SAAO,MAAM,YAAY,yBAAyB,OAAO,WAAW;GAClE,MAAM,WAAW,MAAM,QAAQ,KAAK;IAClC,SAAS,GACN,kCAAA,KACF;IACD;IACD,CAAC;AACF,OAAI,CAAC,SAAS,IAAI;AAChB,UAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;AAInD,QAAI,SAAS,UAAU,KAAK;AAC1B,cACE,8BACA,uEAAuE,SAAS,OAAO,qCACxF;AACD,YAAO,EAAE,SAAS,eAAe;;AAEnC,aACE,8BACA,8EAA8E,SAAS,OAAO,wCAC/F;AACD,WAAO,EAAE,SAAS,eAAe;;GAEnC,MAAM,OAAQ,MAAM,SAAS,MAAM;AACnC,OAAI,KAAK,UAAU,MAAM;AACvB,aACE,0BACA,iGACD;AACD,WAAO,EAAE,SAAS,eAAe;;AAEnC,UAAO;IACL,SAAS;IACT,eACE,OAAO,KAAK,kBAAkB,WAC1B,KAAK,gBACL,KAAA;IACP;IACD;UACK,OAAO;AACd,WACE,8BACA,mFACA,MACD;AACD,SAAO,EAAE,SAAS,eAAe;;;;;;;;AASrC,MAAa,0BAA0B,OACrC,SACA,YACA,YACqD;AACrD,KAAI;AACF,SAAO,MAAM,YAAY,mBAAmB,OAAO,WAAW;GAC5D,MAAM,WAAW,MAAM,QACrB,GAAG,aAAa,sCAChB;IACE,QAAQ;IACR,SAAS;KACP,gBAAgB;MACf,kCAAA;KACF;IACD,MAAM,KAAK,UAAU,QAAQ;IAC7B;IACD,CACF;AACD,OAAI,CAAC,SAAS,IAAI;AAChB,aACE,0BACA,0DAA0D,SAAS,OAAO,IAC3E;AACD,UAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;AACnD;;AAEF,UAAQ,MAAM,SAAS,MAAM;IAC7B;UACK,OAAO;AACd,WACE,6BACA,yFACA,MACD;AACD;;;;;;;;;;;;;AAcJ,MAAM,cAAc,OAClB,IACA,SACe;CACf,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,QAAQ,iBAAiB,WAAW,OAAO,EAAE,GAAG;AACtD,KAAI;AACF,SAAO,MAAM,KAAK,WAAW,OAAO;WAC5B;AACR,eAAa,MAAM","debugId":"89e7f190-1d19-5653-b03e-60d14140c4fc"}
1
+ {"version":3,"file":"sidecar-client.js","sources":["../src/sidecar-client.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n type CaptureEvent,\n type CaptureEventsPayload,\n type OutboundFetchLookupRequest,\n type OutboundFetchLookupResponse,\n type ReplaySessionInfoResponse,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\nimport { sidecarFetch, type SidecarTransport } from \"./sidecar-transport\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n/**\n * Replay calls sit in the request path, unlike record mode's fire-and-forget reporting, so\n * they must be bounded: without a timeout a wedged sidecar would hang every outbound fetch\n * the app makes, and therefore the whole replay, with nothing to look at.\n */\nconst SESSION_INFO_TIMEOUT_MS = 10_000;\nconst LOOKUP_TIMEOUT_MS = 5_000;\n\n/**\n * Capture reporting is off the response path (it runs under `ctx.waitUntil`), so a timeout here\n * is not about latency the app can see — it is about not holding the request context open on a\n * sidecar that will never answer. A refused connection rejects immediately, but a sidecar URL\n * pointing at a host that drops packets rather than refusing them (a firewalled container or LAN\n * address instead of loopback) would otherwise leave the POST pending until the runtime tears the\n * context down, on every captured call.\n *\n * Generous relative to what it bounds: a local sidecar answers a small batch in single-digit\n * milliseconds, so this only ever fires on one that is wedged or unreachable — never on one that\n * is merely busy. Being generous costs nothing because {@link withTimeout} clears the timer the\n * moment the POST settles.\n */\nconst CAPTURE_POST_TIMEOUT_MS = 2_000;\n\n/**\n * The same bound for a service-binding transport, where the sidecar is a Worker that hands the\n * batch to a Durable Object before answering. That object may live in another region and is\n * single-threaded, so a busy one can legitimately take far longer than a loopback process — and\n * failing the report early would drop spans a slightly longer wait would have recorded.\n * `waitUntil` allows 30s past the response, so this stays well inside its budget.\n */\nconst CAPTURE_POST_BINDING_TIMEOUT_MS = 10_000;\n\n/** POSTs capture events to the sidecar. Never rejects — reporting failures only warn (once). */\nexport const postCaptureEvents = async (\n transport: SidecarTransport,\n events: CaptureEvent[],\n): Promise<void> => {\n const timeoutMs =\n transport.kind === \"binding\"\n ? CAPTURE_POST_BINDING_TIMEOUT_MS\n : CAPTURE_POST_TIMEOUT_MS;\n try {\n const payload: CaptureEventsPayload = { events };\n await withTimeout(timeoutMs, async (signal) => {\n const response = await sidecarFetch(\n transport,\n getOriginalFetch(),\n SIDECAR_EVENTS_PATH,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"sidecar-rejected\",\n `Meticulous sidecar rejected capture events (HTTP ${response.status}).`,\n );\n }\n await response.arrayBuffer().catch(() => undefined);\n });\n } catch (error) {\n warnOnce(\n \"sidecar-unreachable\",\n \"Could not reach the Meticulous backend recorder sidecar — backend session events are not being recorded.\",\n error,\n );\n }\n};\n\n/**\n * Outcome of the session handshake.\n *\n * `unavailable` and `unreachable` both mean \"do not replay\", but they are kept apart\n * because only the first is a settled answer. The caller caches per isolate, and caching a\n * one-off network blip would disable mocking for the rest of the session — silently sending\n * every later outbound call to the real service.\n */\nexport type ReplaySessionInfoResult =\n /** The sidecar holds mocks for this session. */\n | { outcome: \"found\"; clockAnchorMs: number | undefined }\n /** The sidecar answered and will never serve this session: no mocks, or no such route. */\n | { outcome: \"unavailable\" }\n /** Could not get an answer — timeout, transport error, 5xx. May succeed later. */\n | { outcome: \"unreachable\" };\n\n/**\n * Asks whether the sidecar holds mocks for a session, and at what instant to freeze the\n * clock while serving it.\n *\n * A 404 is how a record-only sidecar answers, so it counts as a settled `unavailable`\n * rather than a retryable blip.\n */\nexport const getReplaySessionInfo = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<ReplaySessionInfoResult> => {\n const url = `${sidecarUrl}${SIDECAR_REPLAY_SESSION_PATH}?sessionId=${encodeURIComponent(\n frontendSessionId,\n )}`;\n try {\n return await withTimeout(SESSION_INFO_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(url, {\n headers: {\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n signal,\n });\n if (!response.ok) {\n await response.arrayBuffer().catch(() => undefined);\n // 4xx is the sidecar telling us something definitive — no such route (a record-only\n // sidecar), or a protocol version it will not speak. Retrying cannot change either.\n // 5xx may well be a sidecar still finding its feet, so leave it retryable.\n if (response.status >= 500) {\n warnOnce(\n \"replay-session-unreachable\",\n `Meticulous replay sidecar failed the replay session handshake (HTTP ${response.status}) — will retry on the next request.`,\n );\n return { outcome: \"unreachable\" };\n }\n warnOnce(\n \"replay-session-unavailable\",\n `Meticulous replay sidecar did not recognise the replay session route (HTTP ${response.status}) — outbound calls will not be mocked.`,\n );\n return { outcome: \"unavailable\" };\n }\n const info = (await response.json()) as ReplaySessionInfoResponse;\n if (info.found !== true) {\n warnOnce(\n \"replay-session-unknown\",\n \"Meticulous replay sidecar holds no mocks for this session — outbound calls will not be mocked.\",\n );\n return { outcome: \"unavailable\" };\n }\n return {\n outcome: \"found\",\n clockAnchorMs:\n typeof info.clockAnchorMs === \"number\"\n ? info.clockAnchorMs\n : undefined,\n };\n });\n } catch (error) {\n warnOnce(\n \"replay-session-unreachable\",\n \"Could not reach the Meticulous replay sidecar — will retry on the next request.\",\n error,\n );\n return { outcome: \"unreachable\" };\n }\n};\n\n/**\n * Looks up a recorded response for one outbound call. Never rejects; returns undefined when\n * the sidecar could not be consulted, which the caller fails the request on — an unanswered\n * lookup is not evidence that a real call is safe.\n */\nexport const postOutboundFetchLookup = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n payload: OutboundFetchLookupRequest,\n): Promise<OutboundFetchLookupResponse | undefined> => {\n try {\n return await withTimeout(LOOKUP_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(\n `${sidecarUrl}${SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"replay-lookup-rejected\",\n `Meticulous replay sidecar rejected a mock lookup (HTTP ${response.status}).`,\n );\n await response.arrayBuffer().catch(() => undefined);\n return undefined;\n }\n return (await response.json()) as OutboundFetchLookupResponse;\n });\n } catch (error) {\n warnOnce(\n \"replay-lookup-unreachable\",\n \"Could not reach the Meticulous replay sidecar for a mock lookup — the call will fail.\",\n error,\n );\n return undefined;\n }\n};\n\n/**\n * Runs `send` under an abort signal that fires after `ms`, clearing the timer as soon as the\n * call settles — including the response body read, which an abort must still cut short.\n *\n * Deliberately not `AbortSignal.timeout`: its timer cannot be cancelled, and a pending timer\n * keeps workerd's request context alive. Bounding a 5ms POST at 2s would then pin the context\n * for the remaining ~2s on every captured call — worse than the unbounded wait it exists to\n * prevent, and on the healthy path rather than the broken one. `body-capture.ts` uses this\n * same clearable shape, for the same reason.\n */\nconst withTimeout = async <T>(\n ms: number,\n send: (signal: AbortSignal) => Promise<T>,\n): Promise<T> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n try {\n return await send(controller.signal);\n } finally {\n clearTimeout(timer);\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;AAuBA,MAAM,0BAA0B;AAChC,MAAM,oBAAoB;;;;;;;;;;;;;;AAe1B,MAAM,0BAA0B;;;;;;;;AAShC,MAAM,kCAAkC;;AAGxC,MAAa,oBAAoB,OAC/B,WACA,WACkB;CAClB,MAAM,YACJ,UAAU,SAAS,YACf,kCACA;AACN,KAAI;EACF,MAAM,UAAgC,EAAE,QAAQ;AAChD,QAAM,YAAY,WAAW,OAAO,WAAW;GAC7C,MAAM,WAAW,MAAM,aACrB,WACA,kBAAkB,EAClB,qBACA;IACE,QAAQ;IACR,SAAS;KACP,gBAAgB;MACf,kCAAA;KACF;IACD,MAAM,KAAK,UAAU,QAAQ;IAC7B;IACD,CACF;AACD,OAAI,CAAC,SAAS,GACZ,UACE,oBACA,oDAAoD,SAAS,OAAO,IACrE;AAEH,SAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;IACnD;UACK,OAAO;AACd,WACE,uBACA,4GACA,MACD;;;;;;;;;;AA2BL,MAAa,uBAAuB,OAClC,SACA,YACA,sBACqC;CACrC,MAAM,MAAM,GAAG,aAAa,4BAA4B,aAAa,mBACnE,kBACD;AACD,KAAI;AACF,SAAO,MAAM,YAAY,yBAAyB,OAAO,WAAW;GAClE,MAAM,WAAW,MAAM,QAAQ,KAAK;IAClC,SAAS,GACN,kCAAA,KACF;IACD;IACD,CAAC;AACF,OAAI,CAAC,SAAS,IAAI;AAChB,UAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;AAInD,QAAI,SAAS,UAAU,KAAK;AAC1B,cACE,8BACA,uEAAuE,SAAS,OAAO,qCACxF;AACD,YAAO,EAAE,SAAS,eAAe;;AAEnC,aACE,8BACA,8EAA8E,SAAS,OAAO,wCAC/F;AACD,WAAO,EAAE,SAAS,eAAe;;GAEnC,MAAM,OAAQ,MAAM,SAAS,MAAM;AACnC,OAAI,KAAK,UAAU,MAAM;AACvB,aACE,0BACA,iGACD;AACD,WAAO,EAAE,SAAS,eAAe;;AAEnC,UAAO;IACL,SAAS;IACT,eACE,OAAO,KAAK,kBAAkB,WAC1B,KAAK,gBACL,KAAA;IACP;IACD;UACK,OAAO;AACd,WACE,8BACA,mFACA,MACD;AACD,SAAO,EAAE,SAAS,eAAe;;;;;;;;AASrC,MAAa,0BAA0B,OACrC,SACA,YACA,YACqD;AACrD,KAAI;AACF,SAAO,MAAM,YAAY,mBAAmB,OAAO,WAAW;GAC5D,MAAM,WAAW,MAAM,QACrB,GAAG,aAAa,sCAChB;IACE,QAAQ;IACR,SAAS;KACP,gBAAgB;MACf,kCAAA;KACF;IACD,MAAM,KAAK,UAAU,QAAQ;IAC7B;IACD,CACF;AACD,OAAI,CAAC,SAAS,IAAI;AAChB,aACE,0BACA,0DAA0D,SAAS,OAAO,IAC3E;AACD,UAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;AACnD;;AAEF,UAAQ,MAAM,SAAS,MAAM;IAC7B;UACK,OAAO;AACd,WACE,6BACA,yFACA,MACD;AACD;;;;;;;;;;;;;AAcJ,MAAM,cAAc,OAClB,IACA,SACe;CACf,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,QAAQ,iBAAiB,WAAW,OAAO,EAAE,GAAG;AACtD,KAAI;AACF,SAAO,MAAM,KAAK,WAAW,OAAO;WAC5B;AACR,eAAa,MAAM","debugId":"4dc68be9-c4fd-55b2-bd89-c85b8f37cd8d"}
@@ -0,0 +1,38 @@
1
+ //#region src/sidecar-transport.d.ts
2
+ /**
3
+ * Anything with a `fetch` — a Cloudflare service binding (`Fetcher`), or a Durable Object stub.
4
+ * Structural so the shim needs no `@cloudflare/workers-types` dependency.
5
+ */
6
+ interface SidecarFetcher {
7
+ fetch: (...args: unknown[]) => Promise<Response>;
8
+ }
9
+ /**
10
+ * How capture events reach the sidecar.
11
+ *
12
+ * A local `wrangler dev` recording posts to a loopback URL. A deployed Worker cannot: there is
13
+ * no sidecar process at the edge, and one HTTPS round trip per event to a distant host would be
14
+ * both slow and a subrequest per captured call. So a deployed Worker instead talks to a
15
+ * Meticulous sidecar Worker in the same account through a **service binding**, which stays
16
+ * inside the colo and needs neither DNS nor TLS.
17
+ */
18
+ type SidecarTransport = {
19
+ kind: "url";
20
+ url: string;
21
+ } | {
22
+ kind: "binding";
23
+ fetcher: SidecarFetcher;
24
+ instance: object;
25
+ };
26
+ /**
27
+ * Origin the shim addresses a service-binding request to. A service binding delivers to the
28
+ * bound Worker whatever the host is, so only the path is load-bearing — but the URL still has to
29
+ * parse, and using a reserved-by-RFC TLD makes it obvious in a log that no DNS was involved.
30
+ */
31
+ declare const SIDECAR_BINDING_ORIGIN = "https://meticulous-sidecar.invalid";
32
+ /** Env key holding a service binding to the Meticulous recorder sidecar Worker. */
33
+ declare const SIDECAR_BINDING_ENV_KEY = "METICULOUS_SIDECAR";
34
+ /** Env key holding the URL of a local recorder sidecar process. */
35
+ declare const SIDECAR_URL_ENV_KEY = "METICULOUS_SIDECAR_URL";
36
+ //#endregion
37
+ export { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY, SidecarFetcher, SidecarTransport };
38
+ //# sourceMappingURL=sidecar-transport.d.ts.map
@@ -0,0 +1,52 @@
1
+ //#region src/sidecar-transport.ts
2
+ /**
3
+ * Origin the shim addresses a service-binding request to. A service binding delivers to the
4
+ * bound Worker whatever the host is, so only the path is load-bearing — but the URL still has to
5
+ * parse, and using a reserved-by-RFC TLD makes it obvious in a log that no DNS was involved.
6
+ */
7
+
8
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4c3fd120-a382-55e7-8105-c6ff77ac4d6e")}catch(e){}}();
9
+ const SIDECAR_BINDING_ORIGIN = "https://meticulous-sidecar.invalid";
10
+ /** Env key holding a service binding to the Meticulous recorder sidecar Worker. */
11
+ const SIDECAR_BINDING_ENV_KEY = "METICULOUS_SIDECAR";
12
+ /** Env key holding the URL of a local recorder sidecar process. */
13
+ const SIDECAR_URL_ENV_KEY = "METICULOUS_SIDECAR_URL";
14
+ /** The origin the shim's own sidecar requests go to, for the self-capture guards. */
15
+ const transportOrigin = (transport) => transport.kind === "url" ? transport.url : SIDECAR_BINDING_ORIGIN;
16
+ /**
17
+ * Sends one request to the sidecar, whichever transport is in use.
18
+ *
19
+ * `fetchFn` is the unpatched global fetch, passed in rather than imported so this module stays
20
+ * free of the patch machinery; it is unused for a binding, whose `fetch` is its own method.
21
+ */
22
+ const sidecarFetch = (transport, fetchFn, path, init) => {
23
+ const url = `${transportOrigin(transport)}${path}`;
24
+ return transport.kind === "url" ? fetchFn(url, init) : transport.fetcher.fetch(new Request(url, init));
25
+ };
26
+ /**
27
+ * Picks the transport for a recording, or undefined when recording is not configured.
28
+ *
29
+ * A binding wins over a URL. The two are only ever both present by accident — a `.dev.vars`
30
+ * left in an image, say — and the binding is the deliberate one: it can only exist because
31
+ * someone added it to the wrangler configuration of this deployment.
32
+ */
33
+ const resolveSidecarTransport = (options, env) => {
34
+ const envRecord = env !== null && typeof env === "object" ? env : {};
35
+ const binding = options?.sidecarBinding ?? envRecord["METICULOUS_SIDECAR"];
36
+ if (isFetcher(binding)) return {
37
+ kind: "binding",
38
+ fetcher: binding,
39
+ instance: binding
40
+ };
41
+ const rawUrl = options?.sidecarUrl ?? envRecord["METICULOUS_SIDECAR_URL"];
42
+ if (typeof rawUrl === "string" && rawUrl.length > 0) return {
43
+ kind: "url",
44
+ url: rawUrl.replace(/\/+$/, "")
45
+ };
46
+ };
47
+ const isFetcher = (value) => value !== null && typeof value === "object" && typeof value.fetch === "function";
48
+ //#endregion
49
+ export { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY, resolveSidecarTransport, sidecarFetch, transportOrigin };
50
+
51
+ //# sourceMappingURL=sidecar-transport.js.map
52
+ //# debugId=4c3fd120-a382-55e7-8105-c6ff77ac4d6e
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-transport.js","sources":["../src/sidecar-transport.ts"],"sourcesContent":["/**\n * Anything with a `fetch` — a Cloudflare service binding (`Fetcher`), or a Durable Object stub.\n * Structural so the shim needs no `@cloudflare/workers-types` dependency.\n */\nexport interface SidecarFetcher {\n fetch: (...args: unknown[]) => Promise<Response>;\n}\n\n/**\n * How capture events reach the sidecar.\n *\n * A local `wrangler dev` recording posts to a loopback URL. A deployed Worker cannot: there is\n * no sidecar process at the edge, and one HTTPS round trip per event to a distant host would be\n * both slow and a subrequest per captured call. So a deployed Worker instead talks to a\n * Meticulous sidecar Worker in the same account through a **service binding**, which stays\n * inside the colo and needs neither DNS nor TLS.\n */\nexport type SidecarTransport =\n | { kind: \"url\"; url: string }\n | { kind: \"binding\"; fetcher: SidecarFetcher; instance: object };\n\n/**\n * Origin the shim addresses a service-binding request to. A service binding delivers to the\n * bound Worker whatever the host is, so only the path is load-bearing — but the URL still has to\n * parse, and using a reserved-by-RFC TLD makes it obvious in a log that no DNS was involved.\n */\nexport const SIDECAR_BINDING_ORIGIN = \"https://meticulous-sidecar.invalid\";\n\n/** Env key holding a service binding to the Meticulous recorder sidecar Worker. */\nexport const SIDECAR_BINDING_ENV_KEY = \"METICULOUS_SIDECAR\";\n\n/** Env key holding the URL of a local recorder sidecar process. */\nexport const SIDECAR_URL_ENV_KEY = \"METICULOUS_SIDECAR_URL\";\n\n/** The origin the shim's own sidecar requests go to, for the self-capture guards. */\nexport const transportOrigin = (transport: SidecarTransport): string =>\n transport.kind === \"url\" ? transport.url : SIDECAR_BINDING_ORIGIN;\n\n/**\n * Sends one request to the sidecar, whichever transport is in use.\n *\n * `fetchFn` is the unpatched global fetch, passed in rather than imported so this module stays\n * free of the patch machinery; it is unused for a binding, whose `fetch` is its own method.\n */\nexport const sidecarFetch = (\n transport: SidecarTransport,\n fetchFn: typeof globalThis.fetch,\n path: string,\n init: RequestInit,\n): Promise<Response> => {\n const url = `${transportOrigin(transport)}${path}`;\n return transport.kind === \"url\"\n ? fetchFn(url, init)\n : transport.fetcher.fetch(new Request(url, init));\n};\n\nexport interface ResolveSidecarOptions {\n /** Explicit binding, taking precedence over anything found on `env`. */\n sidecarBinding?: SidecarFetcher;\n /** Explicit URL, taking precedence over the env var. */\n sidecarUrl?: string;\n}\n\n/**\n * Picks the transport for a recording, or undefined when recording is not configured.\n *\n * A binding wins over a URL. The two are only ever both present by accident — a `.dev.vars`\n * left in an image, say — and the binding is the deliberate one: it can only exist because\n * someone added it to the wrangler configuration of this deployment.\n */\nexport const resolveSidecarTransport = (\n options: ResolveSidecarOptions | undefined,\n env: unknown,\n): SidecarTransport | undefined => {\n const envRecord =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)\n : {};\n\n const binding = options?.sidecarBinding ?? envRecord[SIDECAR_BINDING_ENV_KEY];\n if (isFetcher(binding)) {\n return { kind: \"binding\", fetcher: binding, instance: binding };\n }\n\n const rawUrl = options?.sidecarUrl ?? envRecord[SIDECAR_URL_ENV_KEY];\n if (typeof rawUrl === \"string\" && rawUrl.length > 0) {\n return { kind: \"url\", url: rawUrl.replace(/\\/+$/, \"\") };\n }\n return undefined;\n};\n\nconst isFetcher = (value: unknown): value is SidecarFetcher & object =>\n value !== null &&\n typeof value === \"object\" &&\n typeof (value as Partial<SidecarFetcher>).fetch === \"function\";\n"],"names":[],"mappings":";;;;;;;;AA0BA,MAAa,yBAAyB;;AAGtC,MAAa,0BAA0B;;AAGvC,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB,cAC9B,UAAU,SAAS,QAAQ,UAAU,MAAM;;;;;;;AAQ7C,MAAa,gBACX,WACA,SACA,MACA,SACsB;CACtB,MAAM,MAAM,GAAG,gBAAgB,UAAU,GAAG;AAC5C,QAAO,UAAU,SAAS,QACtB,QAAQ,KAAK,KAAK,GAClB,UAAU,QAAQ,MAAM,IAAI,QAAQ,KAAK,KAAK,CAAC;;;;;;;;;AAiBrD,MAAa,2BACX,SACA,QACiC;CACjC,MAAM,YACJ,QAAQ,QAAQ,OAAO,QAAQ,WAC1B,MACD,EAAE;CAER,MAAM,UAAU,SAAS,kBAAkB,UAAA;AAC3C,KAAI,UAAU,QAAQ,CACpB,QAAO;EAAE,MAAM;EAAW,SAAS;EAAS,UAAU;EAAS;CAGjE,MAAM,SAAS,SAAS,cAAc,UAAA;AACtC,KAAI,OAAO,WAAW,YAAY,OAAO,SAAS,EAChD,QAAO;EAAE,MAAM;EAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;EAAE;;AAK3D,MAAM,aAAa,UACjB,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAAkC,UAAU","debugId":"4c3fd120-a382-55e7-8105-c6ff77ac4d6e"}
@@ -0,0 +1,84 @@
1
+ //#region src/spans/attributes.d.ts
2
+ /**
3
+ * Span attribute names and technology tags shared by the two recording surfaces.
4
+ *
5
+ * These live here, in the package the in-worker shim ships from, for the same reason
6
+ * `kv-capture.ts` does: a session recorded through workerd and one recorded through the Node
7
+ * recorder have to be attribute-for-attribute identical, or a recording made on one cannot be
8
+ * replayed by the other. `packages/backend-recorder-js/src/constants.ts` re-exports every name
9
+ * below, so the Node recorder keeps one import site and there is one definition.
10
+ */
11
+ /** Tags which client library produced a CLIENT span. Absent on native `node:http` spans. */
12
+ declare const CLIENT_TECHNOLOGY_ATTR = "meticulous.client_technology";
13
+ /** Ties a backend span to the frontend session whose request produced it. */
14
+ declare const FRONTEND_SESSION_ID_ATTR = "meticulous.frontend_session_id";
15
+ /**
16
+ * Where the session id on a span came from. Absent means the browser minted it and sent it on
17
+ * the request, which is the normal case. `"backend"` means we minted it ourselves for a document
18
+ * navigation the browser could not tag (see provisional-session-id.ts), and the page may or may
19
+ * not have gone on to adopt it.
20
+ *
21
+ * The distinction is load-bearing for ingestion: an unadopted backend-minted id names a session
22
+ * that will never exist, and must be treated as *unstamped* so those spans stay eligible for
23
+ * time-window attachment — otherwise stamping them makes attribution worse than leaving them
24
+ * bare. See `hasAnySessionId` in lambda-upload-sessions.
25
+ */
26
+ declare const SESSION_ID_ORIGIN_ATTR = "meticulous.session_id_origin";
27
+ declare const SESSION_ID_ORIGIN_BACKEND = "backend";
28
+ /**
29
+ * An outgoing `fetch` from a Worker. Served back on replay by a `MockStore("workerd-fetch")`
30
+ * living in the replay sidecar, which the in-worker shim queries over plain HTTP (workerd cannot
31
+ * run the Node mock instrumentations).
32
+ *
33
+ * This value travels in recorded session data, so it is a wire contract: the replay orchestrator
34
+ * reads it back as `WORKERD_FETCH_CLIENT_TECHNOLOGY` in `@alwaysmeticulous/api` to decide whether
35
+ * a recording needs a workerd replay sidecar. Keep the two in step.
36
+ */
37
+ declare const CLIENT_TECHNOLOGY_WORKERD_FETCH = "workerd-fetch";
38
+ /**
39
+ * A `fetch` through a Cloudflare binding — a service binding or a Durable Object stub. Recorded
40
+ * from two places: the in-worker shim, and `withMeticulousCloudflareEnv` in a Node process whose
41
+ * bindings come from wrangler's `getPlatformProxy`. Kept distinct from `workerd-fetch` because a
42
+ * binding call never leaves the isolate and its URL is caller-invented, so the two must never be
43
+ * matched against each other.
44
+ */
45
+ declare const CLIENT_TECHNOLOGY_WORKERD_BINDING = "workerd-binding";
46
+ /**
47
+ * An operation on a Cloudflare KV namespace binding, from the same two places. Its own technology
48
+ * because a KV operation is not Request/Response-shaped at all — there is no method, URL or status
49
+ * to match on, only a binding, an operation and a key.
50
+ */
51
+ declare const CLIENT_TECHNOLOGY_WORKERD_KV = "workerd-kv";
52
+ /**
53
+ * A postgres.js query, whether captured in Node (by the require hook or `withMeticulousPostgres`)
54
+ * or in a deployed Worker. Deliberately distinct from `postgres` (node-postgres) so the two stores
55
+ * never match each other's spans even though both talk to Postgres.
56
+ */
57
+ declare const CLIENT_TECHNOLOGY_POSTGRES_JS = "postgres-js";
58
+ /**
59
+ * The `env` key a binding call went through. Absent when the binding instance was never seen on
60
+ * `env` — most commonly a Durable Object stub, which `namespace.get()` returns. Shared by
61
+ * `fetch`-shaped binding spans and KV spans: both identify the binding this way.
62
+ */
63
+ declare const WORKERD_BINDING_NAME_ATTR = "meticulous.workerd.binding";
64
+ /** Which KV method was called: `get`, `getWithMetadata`, `put`, `delete` or `list`. */
65
+ declare const WORKERD_KV_OPERATION_ATTR = "meticulous.workerd.kv.operation";
66
+ /** The key operated on. Absent for `list` and for a bulk `get`, whose keys are in the args. */
67
+ declare const WORKERD_KV_KEY_ATTR = "meticulous.workerd.kv.key";
68
+ /** JSON of the call's arguments; a `put` value appears as null (it lives in the value attr). */
69
+ declare const WORKERD_KV_ARGS_ATTR = "meticulous.workerd.kv.args";
70
+ declare const WORKERD_KV_ARGS_TRUNCATED_ATTR = "meticulous.workerd.kv.args.truncated";
71
+ /** JSON of the value a `put` wrote, with secret-looking fields redacted. */
72
+ declare const WORKERD_KV_VALUE_ATTR = "meticulous.workerd.kv.value";
73
+ declare const WORKERD_KV_VALUE_TRUNCATED_ATTR = "meticulous.workerd.kv.value.truncated";
74
+ /** JSON of what the operation returned. Absent for `put`/`delete`, which return nothing. */
75
+ declare const WORKERD_KV_RESULT_ATTR = "meticulous.workerd.kv.result";
76
+ declare const WORKERD_KV_RESULT_TRUNCATED_ATTR = "meticulous.workerd.kv.result.truncated";
77
+ /**
78
+ * Why a value is missing: `"stream"` (reading it would take the bytes from the app) or `"binary"`
79
+ * (skipped deliberately — KV blobs are large and not UTF-8).
80
+ */
81
+ declare const WORKERD_KV_OMITTED_ATTR = "meticulous.workerd.kv.omitted";
82
+ //#endregion
83
+ export { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR };
84
+ //# sourceMappingURL=attributes.d.ts.map
@@ -0,0 +1,88 @@
1
+ //#region src/spans/attributes.ts
2
+ /**
3
+ * Span attribute names and technology tags shared by the two recording surfaces.
4
+ *
5
+ * These live here, in the package the in-worker shim ships from, for the same reason
6
+ * `kv-capture.ts` does: a session recorded through workerd and one recorded through the Node
7
+ * recorder have to be attribute-for-attribute identical, or a recording made on one cannot be
8
+ * replayed by the other. `packages/backend-recorder-js/src/constants.ts` re-exports every name
9
+ * below, so the Node recorder keeps one import site and there is one definition.
10
+ */
11
+ /** Tags which client library produced a CLIENT span. Absent on native `node:http` spans. */
12
+
13
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9c7bae2d-7e8a-57e2-9065-56765d8c547b")}catch(e){}}();
14
+ const CLIENT_TECHNOLOGY_ATTR = "meticulous.client_technology";
15
+ /** Ties a backend span to the frontend session whose request produced it. */
16
+ const FRONTEND_SESSION_ID_ATTR = "meticulous.frontend_session_id";
17
+ /**
18
+ * Where the session id on a span came from. Absent means the browser minted it and sent it on
19
+ * the request, which is the normal case. `"backend"` means we minted it ourselves for a document
20
+ * navigation the browser could not tag (see provisional-session-id.ts), and the page may or may
21
+ * not have gone on to adopt it.
22
+ *
23
+ * The distinction is load-bearing for ingestion: an unadopted backend-minted id names a session
24
+ * that will never exist, and must be treated as *unstamped* so those spans stay eligible for
25
+ * time-window attachment — otherwise stamping them makes attribution worse than leaving them
26
+ * bare. See `hasAnySessionId` in lambda-upload-sessions.
27
+ */
28
+ const SESSION_ID_ORIGIN_ATTR = "meticulous.session_id_origin";
29
+ const SESSION_ID_ORIGIN_BACKEND = "backend";
30
+ /**
31
+ * An outgoing `fetch` from a Worker. Served back on replay by a `MockStore("workerd-fetch")`
32
+ * living in the replay sidecar, which the in-worker shim queries over plain HTTP (workerd cannot
33
+ * run the Node mock instrumentations).
34
+ *
35
+ * This value travels in recorded session data, so it is a wire contract: the replay orchestrator
36
+ * reads it back as `WORKERD_FETCH_CLIENT_TECHNOLOGY` in `@alwaysmeticulous/api` to decide whether
37
+ * a recording needs a workerd replay sidecar. Keep the two in step.
38
+ */
39
+ const CLIENT_TECHNOLOGY_WORKERD_FETCH = "workerd-fetch";
40
+ /**
41
+ * A `fetch` through a Cloudflare binding — a service binding or a Durable Object stub. Recorded
42
+ * from two places: the in-worker shim, and `withMeticulousCloudflareEnv` in a Node process whose
43
+ * bindings come from wrangler's `getPlatformProxy`. Kept distinct from `workerd-fetch` because a
44
+ * binding call never leaves the isolate and its URL is caller-invented, so the two must never be
45
+ * matched against each other.
46
+ */
47
+ const CLIENT_TECHNOLOGY_WORKERD_BINDING = "workerd-binding";
48
+ /**
49
+ * An operation on a Cloudflare KV namespace binding, from the same two places. Its own technology
50
+ * because a KV operation is not Request/Response-shaped at all — there is no method, URL or status
51
+ * to match on, only a binding, an operation and a key.
52
+ */
53
+ const CLIENT_TECHNOLOGY_WORKERD_KV = "workerd-kv";
54
+ /**
55
+ * A postgres.js query, whether captured in Node (by the require hook or `withMeticulousPostgres`)
56
+ * or in a deployed Worker. Deliberately distinct from `postgres` (node-postgres) so the two stores
57
+ * never match each other's spans even though both talk to Postgres.
58
+ */
59
+ const CLIENT_TECHNOLOGY_POSTGRES_JS = "postgres-js";
60
+ /**
61
+ * The `env` key a binding call went through. Absent when the binding instance was never seen on
62
+ * `env` — most commonly a Durable Object stub, which `namespace.get()` returns. Shared by
63
+ * `fetch`-shaped binding spans and KV spans: both identify the binding this way.
64
+ */
65
+ const WORKERD_BINDING_NAME_ATTR = "meticulous.workerd.binding";
66
+ /** Which KV method was called: `get`, `getWithMetadata`, `put`, `delete` or `list`. */
67
+ const WORKERD_KV_OPERATION_ATTR = "meticulous.workerd.kv.operation";
68
+ /** The key operated on. Absent for `list` and for a bulk `get`, whose keys are in the args. */
69
+ const WORKERD_KV_KEY_ATTR = "meticulous.workerd.kv.key";
70
+ /** JSON of the call's arguments; a `put` value appears as null (it lives in the value attr). */
71
+ const WORKERD_KV_ARGS_ATTR = "meticulous.workerd.kv.args";
72
+ const WORKERD_KV_ARGS_TRUNCATED_ATTR = "meticulous.workerd.kv.args.truncated";
73
+ /** JSON of the value a `put` wrote, with secret-looking fields redacted. */
74
+ const WORKERD_KV_VALUE_ATTR = "meticulous.workerd.kv.value";
75
+ const WORKERD_KV_VALUE_TRUNCATED_ATTR = "meticulous.workerd.kv.value.truncated";
76
+ /** JSON of what the operation returned. Absent for `put`/`delete`, which return nothing. */
77
+ const WORKERD_KV_RESULT_ATTR = "meticulous.workerd.kv.result";
78
+ const WORKERD_KV_RESULT_TRUNCATED_ATTR = "meticulous.workerd.kv.result.truncated";
79
+ /**
80
+ * Why a value is missing: `"stream"` (reading it would take the bytes from the app) or `"binary"`
81
+ * (skipped deliberately — KV blobs are large and not UTF-8).
82
+ */
83
+ const WORKERD_KV_OMITTED_ATTR = "meticulous.workerd.kv.omitted";
84
+ //#endregion
85
+ export { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR };
86
+
87
+ //# sourceMappingURL=attributes.js.map
88
+ //# debugId=9c7bae2d-7e8a-57e2-9065-56765d8c547b
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attributes.js","sources":["../../src/spans/attributes.ts"],"sourcesContent":["/**\n * Span attribute names and technology tags shared by the two recording surfaces.\n *\n * These live here, in the package the in-worker shim ships from, for the same reason\n * `kv-capture.ts` does: a session recorded through workerd and one recorded through the Node\n * recorder have to be attribute-for-attribute identical, or a recording made on one cannot be\n * replayed by the other. `packages/backend-recorder-js/src/constants.ts` re-exports every name\n * below, so the Node recorder keeps one import site and there is one definition.\n */\n\n/** Tags which client library produced a CLIENT span. Absent on native `node:http` spans. */\nexport const CLIENT_TECHNOLOGY_ATTR = \"meticulous.client_technology\";\n\n/** Ties a backend span to the frontend session whose request produced it. */\nexport const FRONTEND_SESSION_ID_ATTR = \"meticulous.frontend_session_id\";\n\n/**\n * Where the session id on a span came from. Absent means the browser minted it and sent it on\n * the request, which is the normal case. `\"backend\"` means we minted it ourselves for a document\n * navigation the browser could not tag (see provisional-session-id.ts), and the page may or may\n * not have gone on to adopt it.\n *\n * The distinction is load-bearing for ingestion: an unadopted backend-minted id names a session\n * that will never exist, and must be treated as *unstamped* so those spans stay eligible for\n * time-window attachment — otherwise stamping them makes attribution worse than leaving them\n * bare. See `hasAnySessionId` in lambda-upload-sessions.\n */\nexport const SESSION_ID_ORIGIN_ATTR = \"meticulous.session_id_origin\";\nexport const SESSION_ID_ORIGIN_BACKEND = \"backend\";\n\n/**\n * An outgoing `fetch` from a Worker. Served back on replay by a `MockStore(\"workerd-fetch\")`\n * living in the replay sidecar, which the in-worker shim queries over plain HTTP (workerd cannot\n * run the Node mock instrumentations).\n *\n * This value travels in recorded session data, so it is a wire contract: the replay orchestrator\n * reads it back as `WORKERD_FETCH_CLIENT_TECHNOLOGY` in `@alwaysmeticulous/api` to decide whether\n * a recording needs a workerd replay sidecar. Keep the two in step.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_FETCH = \"workerd-fetch\";\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding or a Durable Object stub. Recorded\n * from two places: the in-worker shim, and `withMeticulousCloudflareEnv` in a Node process whose\n * bindings come from wrangler's `getPlatformProxy`. Kept distinct from `workerd-fetch` because a\n * binding call never leaves the isolate and its URL is caller-invented, so the two must never be\n * matched against each other.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_BINDING = \"workerd-binding\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, from the same two places. Its own technology\n * because a KV operation is not Request/Response-shaped at all — there is no method, URL or status\n * to match on, only a binding, an operation and a key.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_KV = \"workerd-kv\";\n\n/**\n * A postgres.js query, whether captured in Node (by the require hook or `withMeticulousPostgres`)\n * or in a deployed Worker. Deliberately distinct from `postgres` (node-postgres) so the two stores\n * never match each other's spans even though both talk to Postgres.\n */\nexport const CLIENT_TECHNOLOGY_POSTGRES_JS = \"postgres-js\";\n\n/**\n * The `env` key a binding call went through. Absent when the binding instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` returns. Shared by\n * `fetch`-shaped binding spans and KV spans: both identify the binding this way.\n */\nexport const WORKERD_BINDING_NAME_ATTR = \"meticulous.workerd.binding\";\n\n/** Which KV method was called: `get`, `getWithMetadata`, `put`, `delete` or `list`. */\nexport const WORKERD_KV_OPERATION_ATTR = \"meticulous.workerd.kv.operation\";\n/** The key operated on. Absent for `list` and for a bulk `get`, whose keys are in the args. */\nexport const WORKERD_KV_KEY_ATTR = \"meticulous.workerd.kv.key\";\n/** JSON of the call's arguments; a `put` value appears as null (it lives in the value attr). */\nexport const WORKERD_KV_ARGS_ATTR = \"meticulous.workerd.kv.args\";\nexport const WORKERD_KV_ARGS_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.args.truncated\";\n/** JSON of the value a `put` wrote, with secret-looking fields redacted. */\nexport const WORKERD_KV_VALUE_ATTR = \"meticulous.workerd.kv.value\";\nexport const WORKERD_KV_VALUE_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.value.truncated\";\n/** JSON of what the operation returned. Absent for `put`/`delete`, which return nothing. */\nexport const WORKERD_KV_RESULT_ATTR = \"meticulous.workerd.kv.result\";\nexport const WORKERD_KV_RESULT_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.result.truncated\";\n/**\n * Why a value is missing: `\"stream\"` (reading it would take the bytes from the app) or `\"binary\"`\n * (skipped deliberately — KV blobs are large and not UTF-8).\n */\nexport const WORKERD_KV_OMITTED_ATTR = \"meticulous.workerd.kv.omitted\";\n"],"names":[],"mappings":";;;;;;;;;;;;;AAWA,MAAa,yBAAyB;;AAGtC,MAAa,2BAA2B;;;;;;;;;;;;AAaxC,MAAa,yBAAyB;AACtC,MAAa,4BAA4B;;;;;;;;;;AAWzC,MAAa,kCAAkC;;;;;;;;AAS/C,MAAa,oCAAoC;;;;;;AAOjD,MAAa,+BAA+B;;;;;;AAO5C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,4BAA4B;;AAGzC,MAAa,4BAA4B;;AAEzC,MAAa,sBAAsB;;AAEnC,MAAa,uBAAuB;AACpC,MAAa,iCACX;;AAEF,MAAa,wBAAwB;AACrC,MAAa,kCACX;;AAEF,MAAa,yBAAyB;AACtC,MAAa,mCACX;;;;;AAKF,MAAa,0BAA0B","debugId":"9c7bae2d-7e8a-57e2-9065-56765d8c547b"}
@@ -0,0 +1,78 @@
1
+ //#region src/spans/readable-span.d.ts
2
+ /**
3
+ * The span shape both recording surfaces produce, and the serialization that turns it into the
4
+ * JSON a recorded session is stored as.
5
+ *
6
+ * Lives here rather than in the Node recorder because a Worker-hosted sidecar has to write the
7
+ * same wire format from inside workerd, where neither `@opentelemetry/api` nor the Node
8
+ * recorder's bundle can be loaded. `packages/backend-recorder-js/src/exporters/span-serializer.ts`
9
+ * re-exports all of this and layers its richer, per-instrumentation attribute types on top, so
10
+ * the *logic* has exactly one implementation while the documentation types stay where they are
11
+ * used.
12
+ */
13
+ /**
14
+ * OpenTelemetry's `SpanKind`, inlined. The shim has no runtime dependencies — pulling in
15
+ * `@opentelemetry/api` for two numeric enums would put it in every customer's worker bundle.
16
+ */
17
+ declare const SPAN_KIND_INTERNAL = 0;
18
+ declare const SPAN_KIND_SERVER = 1;
19
+ declare const SPAN_KIND_CLIENT = 2;
20
+ /** OpenTelemetry's `SpanStatusCode`, inlined for the same reason. */
21
+ declare const SPAN_STATUS_UNSET = 0;
22
+ declare const SPAN_STATUS_OK = 1;
23
+ declare const SPAN_STATUS_ERROR = 2;
24
+ interface SpanStatus {
25
+ code: number;
26
+ message?: string;
27
+ }
28
+ /** `[seconds, nanoseconds]` — OpenTelemetry's `HrTime`. */
29
+ type HrTime = [number, number];
30
+ interface ReadableSpan {
31
+ name: string;
32
+ /**
33
+ * Set by the hand-built spans in `span-builder.ts`. OTel-SDK spans carry the parent in
34
+ * {@link ReadableSpan.parentSpanContext} instead.
35
+ */
36
+ parentSpanId?: string;
37
+ /**
38
+ * How an OTel-SDK span reports its parent since `@opentelemetry/sdk-trace-base` v2, which
39
+ * dropped the flat `parentSpanId` field.
40
+ */
41
+ parentSpanContext?: {
42
+ spanId: string;
43
+ };
44
+ kind: number;
45
+ status: unknown;
46
+ startTime: HrTime;
47
+ endTime: HrTime;
48
+ duration: HrTime;
49
+ attributes?: Record<string, unknown>;
50
+ spanContext(): {
51
+ traceId: string;
52
+ spanId: string;
53
+ };
54
+ }
55
+ /**
56
+ * A serialized span, with the attribute bag left open. The Node recorder narrows `attributes`
57
+ * to its union of per-instrumentation contracts; nothing about the wire format depends on that
58
+ * narrowing.
59
+ */
60
+ interface SerializedSpanBase {
61
+ name: string;
62
+ traceId: string;
63
+ spanId: string;
64
+ parentSpanId: string | undefined;
65
+ kind: number;
66
+ status: unknown;
67
+ startTimeMs: number;
68
+ endTimeMs: number;
69
+ durationMs: number;
70
+ clientTechnology?: string;
71
+ frontendSessionId?: string;
72
+ attributes: Record<string, unknown>;
73
+ }
74
+ declare const hrTimeToMs: (hrTime: HrTime) => number;
75
+ declare const msToHrTime: (ms: number) => HrTime;
76
+ //#endregion
77
+ export { HrTime, ReadableSpan, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, SerializedSpanBase, SpanStatus, hrTimeToMs, msToHrTime };
78
+ //# sourceMappingURL=readable-span.d.ts.map
@@ -0,0 +1,35 @@
1
+ //#region src/spans/readable-span.ts
2
+ /**
3
+ * The span shape both recording surfaces produce, and the serialization that turns it into the
4
+ * JSON a recorded session is stored as.
5
+ *
6
+ * Lives here rather than in the Node recorder because a Worker-hosted sidecar has to write the
7
+ * same wire format from inside workerd, where neither `@opentelemetry/api` nor the Node
8
+ * recorder's bundle can be loaded. `packages/backend-recorder-js/src/exporters/span-serializer.ts`
9
+ * re-exports all of this and layers its richer, per-instrumentation attribute types on top, so
10
+ * the *logic* has exactly one implementation while the documentation types stay where they are
11
+ * used.
12
+ */
13
+ /**
14
+ * OpenTelemetry's `SpanKind`, inlined. The shim has no runtime dependencies — pulling in
15
+ * `@opentelemetry/api` for two numeric enums would put it in every customer's worker bundle.
16
+ */
17
+
18
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4b8da1fb-2c09-5543-acf1-a30e8b358116")}catch(e){}}();
19
+ const SPAN_KIND_INTERNAL = 0;
20
+ const SPAN_KIND_SERVER = 1;
21
+ const SPAN_KIND_CLIENT = 2;
22
+ /** OpenTelemetry's `SpanStatusCode`, inlined for the same reason. */
23
+ const SPAN_STATUS_UNSET = 0;
24
+ const SPAN_STATUS_OK = 1;
25
+ const SPAN_STATUS_ERROR = 2;
26
+ const hrTimeToMs = (hrTime) => hrTime[0] * 1e3 + hrTime[1] / 1e6;
27
+ const msToHrTime = (ms) => {
28
+ const seconds = Math.floor(ms / 1e3);
29
+ return [seconds, Math.round((ms - seconds * 1e3) * 1e6)];
30
+ };
31
+ //#endregion
32
+ export { SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, hrTimeToMs, msToHrTime };
33
+
34
+ //# sourceMappingURL=readable-span.js.map
35
+ //# debugId=4b8da1fb-2c09-5543-acf1-a30e8b358116
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readable-span.js","sources":["../../src/spans/readable-span.ts"],"sourcesContent":["/**\n * The span shape both recording surfaces produce, and the serialization that turns it into the\n * JSON a recorded session is stored as.\n *\n * Lives here rather than in the Node recorder because a Worker-hosted sidecar has to write the\n * same wire format from inside workerd, where neither `@opentelemetry/api` nor the Node\n * recorder's bundle can be loaded. `packages/backend-recorder-js/src/exporters/span-serializer.ts`\n * re-exports all of this and layers its richer, per-instrumentation attribute types on top, so\n * the *logic* has exactly one implementation while the documentation types stay where they are\n * used.\n */\n\n/**\n * OpenTelemetry's `SpanKind`, inlined. The shim has no runtime dependencies — pulling in\n * `@opentelemetry/api` for two numeric enums would put it in every customer's worker bundle.\n */\nexport const SPAN_KIND_INTERNAL = 0;\nexport const SPAN_KIND_SERVER = 1;\nexport const SPAN_KIND_CLIENT = 2;\n\n/** OpenTelemetry's `SpanStatusCode`, inlined for the same reason. */\nexport const SPAN_STATUS_UNSET = 0;\nexport const SPAN_STATUS_OK = 1;\nexport const SPAN_STATUS_ERROR = 2;\n\nexport interface SpanStatus {\n code: number;\n message?: string;\n}\n\n/** `[seconds, nanoseconds]` — OpenTelemetry's `HrTime`. */\nexport type HrTime = [number, number];\n\nexport interface ReadableSpan {\n name: string;\n /**\n * Set by the hand-built spans in `span-builder.ts`. OTel-SDK spans carry the parent in\n * {@link ReadableSpan.parentSpanContext} instead.\n */\n parentSpanId?: string;\n /**\n * How an OTel-SDK span reports its parent since `@opentelemetry/sdk-trace-base` v2, which\n * dropped the flat `parentSpanId` field.\n */\n parentSpanContext?: { spanId: string };\n kind: number;\n status: unknown;\n startTime: HrTime;\n endTime: HrTime;\n duration: HrTime;\n attributes?: Record<string, unknown>;\n spanContext(): { traceId: string; spanId: string };\n}\n\n/**\n * A serialized span, with the attribute bag left open. The Node recorder narrows `attributes`\n * to its union of per-instrumentation contracts; nothing about the wire format depends on that\n * narrowing.\n */\nexport interface SerializedSpanBase {\n name: string;\n traceId: string;\n spanId: string;\n parentSpanId: string | undefined;\n kind: number;\n status: unknown;\n startTimeMs: number;\n endTimeMs: number;\n durationMs: number;\n clientTechnology?: string;\n frontendSessionId?: string;\n attributes: Record<string, unknown>;\n}\n\nexport const hrTimeToMs = (hrTime: HrTime): number =>\n hrTime[0] * 1000 + hrTime[1] / 1e6;\n\nexport const msToHrTime = (ms: number): HrTime => {\n const seconds = Math.floor(ms / 1000);\n return [seconds, Math.round((ms - seconds * 1000) * 1e6)];\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,qBAAqB;AAClC,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;;AAGhC,MAAa,oBAAoB;AACjC,MAAa,iBAAiB;AAC9B,MAAa,oBAAoB;AAmDjC,MAAa,cAAc,WACzB,OAAO,KAAK,MAAO,OAAO,KAAK;AAEjC,MAAa,cAAc,OAAuB;CAChD,MAAM,UAAU,KAAK,MAAM,KAAK,IAAK;AACrC,QAAO,CAAC,SAAS,KAAK,OAAO,KAAK,UAAU,OAAQ,IAAI,CAAC","debugId":"4b8da1fb-2c09-5543-acf1-a30e8b358116"}
@@ -0,0 +1,14 @@
1
+ import { ReadableSpan, SerializedSpanBase } from "./readable-span.js";
2
+
3
+ //#region src/spans/serialize-span.d.ts
4
+ /**
5
+ * Turns a span into the JSON shape a recorded backend session stores.
6
+ *
7
+ * Shared by the Node exporter and the Worker-hosted sidecar so the two cannot drift: ingestion
8
+ * and every replay mock store read these field names, and a sidecar writing a near-miss of
9
+ * this shape would produce recordings that load but never match.
10
+ */
11
+ declare const serializeSpan: (span: ReadableSpan) => SerializedSpanBase;
12
+ //#endregion
13
+ export { serializeSpan };
14
+ //# sourceMappingURL=serialize-span.d.ts.map
@@ -0,0 +1,38 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e12748a8-3496-5830-8c8f-229beae7068b")}catch(e){}}();
3
+ import { CLIENT_TECHNOLOGY_ATTR, FRONTEND_SESSION_ID_ATTR } from "./attributes.js";
4
+ import { hrTimeToMs } from "./readable-span.js";
5
+ //#region src/spans/serialize-span.ts
6
+ /**
7
+ * Turns a span into the JSON shape a recorded backend session stores.
8
+ *
9
+ * Shared by the Node exporter and the Worker-hosted sidecar so the two cannot drift: ingestion
10
+ * and every replay mock store read these field names, and a sidecar writing a near-miss of
11
+ * this shape would produce recordings that load but never match.
12
+ */
13
+ const serializeSpan = (span) => {
14
+ const ctx = span.spanContext();
15
+ const attributes = { ...span.attributes ?? {} };
16
+ const clientTechnology = attributes[CLIENT_TECHNOLOGY_ATTR];
17
+ const frontendSessionId = attributes[FRONTEND_SESSION_ID_ATTR];
18
+ delete attributes[CLIENT_TECHNOLOGY_ATTR];
19
+ return {
20
+ name: span.name,
21
+ traceId: ctx.traceId,
22
+ spanId: ctx.spanId,
23
+ parentSpanId: span.parentSpanId ?? span.parentSpanContext?.spanId,
24
+ kind: span.kind,
25
+ status: span.status,
26
+ startTimeMs: hrTimeToMs(span.startTime),
27
+ endTimeMs: hrTimeToMs(span.endTime),
28
+ durationMs: hrTimeToMs(span.duration),
29
+ ...clientTechnology ? { clientTechnology } : {},
30
+ ...frontendSessionId ? { frontendSessionId } : {},
31
+ attributes
32
+ };
33
+ };
34
+ //#endregion
35
+ export { serializeSpan };
36
+
37
+ //# sourceMappingURL=serialize-span.js.map
38
+ //# debugId=e12748a8-3496-5830-8c8f-229beae7068b
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize-span.js","sources":["../../src/spans/serialize-span.ts"],"sourcesContent":["import { CLIENT_TECHNOLOGY_ATTR, FRONTEND_SESSION_ID_ATTR } from \"./attributes\";\nimport {\n hrTimeToMs,\n type ReadableSpan,\n type SerializedSpanBase,\n} from \"./readable-span\";\n\n/**\n * Turns a span into the JSON shape a recorded backend session stores.\n *\n * Shared by the Node exporter and the Worker-hosted sidecar so the two cannot drift: ingestion\n * and every replay mock store read these field names, and a sidecar writing a near-miss of\n * this shape would produce recordings that load but never match.\n */\nexport const serializeSpan = (span: ReadableSpan): SerializedSpanBase => {\n const ctx = span.spanContext();\n // Copy so we never mutate the live OTel span's attributes.\n const attributes = { ...(span.attributes ?? {}) };\n\n const clientTechnology = attributes[CLIENT_TECHNOLOGY_ATTR] as\n | string\n | undefined;\n const frontendSessionId = attributes[FRONTEND_SESSION_ID_ATTR] as\n | string\n | undefined;\n\n // client_technology moves to the top level; frontend_session_id is copied (left in place for\n // backward compatibility — see download-sessions.ts in the webapp repo).\n delete attributes[CLIENT_TECHNOLOGY_ATTR];\n\n return {\n name: span.name,\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n parentSpanId: span.parentSpanId ?? span.parentSpanContext?.spanId,\n kind: span.kind,\n status: span.status,\n startTimeMs: hrTimeToMs(span.startTime),\n endTimeMs: hrTimeToMs(span.endTime),\n durationMs: hrTimeToMs(span.duration),\n // Spread conditionally so we never emit `key: undefined` (exactOptionalPropertyTypes).\n ...(clientTechnology ? { clientTechnology } : {}),\n ...(frontendSessionId ? { frontendSessionId } : {}),\n attributes,\n };\n};\n"],"names":[],"mappings":";;;;;;;;;;;;AAcA,MAAa,iBAAiB,SAA2C;CACvE,MAAM,MAAM,KAAK,aAAa;CAE9B,MAAM,aAAa,EAAE,GAAI,KAAK,cAAc,EAAE,EAAG;CAEjD,MAAM,mBAAmB,WAAW;CAGpC,MAAM,oBAAoB,WAAW;AAMrC,QAAO,WAAW;AAElB,QAAO;EACL,MAAM,KAAK;EACX,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,cAAc,KAAK,gBAAgB,KAAK,mBAAmB;EAC3D,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,aAAa,WAAW,KAAK,UAAU;EACvC,WAAW,WAAW,KAAK,QAAQ;EACnC,YAAY,WAAW,KAAK,SAAS;EAErC,GAAI,mBAAmB,EAAE,kBAAkB,GAAG,EAAE;EAChD,GAAI,oBAAoB,EAAE,mBAAmB,GAAG,EAAE;EAClD;EACD","debugId":"e12748a8-3496-5830-8c8f-229beae7068b"}
@@ -0,0 +1,61 @@
1
+ import { CaptureEvent } from "../protocol.js";
2
+ import { ReadableSpan } from "./readable-span.js";
3
+
4
+ //#region src/spans/span-builder.d.ts
5
+ /**
6
+ * Builds spans from shim capture events.
7
+ *
8
+ * Each inbound request becomes a SERVER span and the root of its own trace; every other event of
9
+ * that request becomes a CLIENT span parented under it. The shim supplies the trace and
10
+ * server-span ids (see `CorrelatedEvent`), so building a span needs no memory of earlier events
11
+ * — which is what lets a Worker-hosted sidecar be evicted between two batches of one request
12
+ * without splitting its trace.
13
+ *
14
+ * The `requestId → ids` fallback below exists only for a shim that predates those fields. It is
15
+ * the reason this is a class rather than a function.
16
+ *
17
+ * Shared with the Node recorder's sidecar (`packages/backend-recorder-js/src/sidecar/`), which
18
+ * re-exports it: both sidecars must produce identical spans or a recording made through one
19
+ * cannot be replayed by the machinery built for the other.
20
+ */
21
+ declare class SpanBuilder {
22
+ /**
23
+ * Legacy fallback ids, keyed by `requestId`. Outbound events usually arrive BEFORE their
24
+ * inbound event (the inbound is only reported once the handler completes), so an entry is
25
+ * created lazily on first sight from either event kind.
26
+ */
27
+ private readonly traceContexts;
28
+ build(event: CaptureEvent): ReadableSpan | null;
29
+ private buildInboundSpan;
30
+ private buildOutboundSpan;
31
+ /**
32
+ * A binding call. Named after the binding rather than the bare method, since the `env` key is
33
+ * what identifies the call — the URL is whatever the app invented.
34
+ */
35
+ private buildBindingSpan;
36
+ /**
37
+ * A KV operation. Named `kv.<binding>.<operation>` after the Prisma convention
38
+ * (`prisma.<model>.<operation>`) rather than the `METHOD TARGET` shape the HTTP-like spans use
39
+ * — nothing about a KV call is an HTTP method, and the dotted name keeps a `get` on a
40
+ * namespace distinguishable from a `GET` through a service binding at a glance.
41
+ */
42
+ private buildKvSpan;
43
+ /**
44
+ * A postgres.js query. Name and attributes match what the Node instrumentation emits
45
+ * (`postgresjs.<command>`), because the same store loads both.
46
+ */
47
+ private buildPostgresSpan;
48
+ private buildHttpClientSpan;
49
+ private buildClientSpan;
50
+ /**
51
+ * The trace and server-span ids for an event: the shim's, when it sent them, and otherwise a
52
+ * pair invented per `requestId` and remembered — the pre-`traceId` behaviour.
53
+ */
54
+ private resolveIds;
55
+ private pruneTraceContexts;
56
+ }
57
+ /** WebCrypto rather than `node:crypto`, so this runs in a Worker as well as in Node. */
58
+ declare const randomHex: (bytes: number) => string;
59
+ //#endregion
60
+ export { SpanBuilder, randomHex };
61
+ //# sourceMappingURL=span-builder.d.ts.map