diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts index 2d3bdec81..87845a694 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts @@ -4075,10 +4075,14 @@ export class _HexclaveClientAppImplIncomplete { - const context = this._getSpanPropagationContext(options?.parentIds); + getSpanPropagationHeaders(options?: { parentIds?: ParentRef[], root?: boolean }): Record { + const context = this._getSpanPropagationContext(options?.parentIds, options?.root); if (!context) return {}; return { [SPAN_CONTEXT_HEADER]: encodeSpanContextHeader(context) }; } diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.test.ts b/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.test.ts index cec210d01..a901adc81 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.test.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.test.ts @@ -242,6 +242,24 @@ describe("installFetchSpanPropagation", () => { expect(sentHeader()).toBeNull(); }); + it("never overwrites an explicitly-set span-context header (init.headers)", async () => { + install({ projectId: "p", sessionReplaySegmentId: SEG }); + const explicit = encodeSpanContextHeader({ projectId: "p", customParentSpanIds: [CUSTOM_A] }); + await (globalThis as { fetch: typeof fetch }).fetch("/x", { headers: { [SPAN_CONTEXT_HEADER]: explicit } }); + // Passed through untouched: the wrapper must not clobber the caller's precise intent. + expect(new Headers(calls[0].init?.headers).get(SPAN_CONTEXT_HEADER)).toBe(explicit); + }); + + it("never overwrites an explicitly-set span-context header (Request headers)", async () => { + install({ projectId: "p", sessionReplaySegmentId: SEG }); + const explicit = encodeSpanContextHeader({ projectId: "p", customParentSpanIds: [CUSTOM_B] }); + const req = new Request("https://app.example.com/x", { headers: { [SPAN_CONTEXT_HEADER]: explicit } }); + await (globalThis as { fetch: typeof fetch }).fetch(req); + // No init constructed — the Request (with its own header) passes through as-is. + expect(calls[0].init?.headers).toBeUndefined(); + expect((calls[0].input as Request).headers.get(SPAN_CONTEXT_HEADER)).toBe(explicit); + }); + it("is idempotent and uninstalls cleanly", async () => { install({ projectId: "p", sessionReplaySegmentId: SEG }); const second = installFetchSpanPropagation({ getContext: () => null, getSelfOrigin: () => SELF, getAllowedOrigins: () => [] }); diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.ts b/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.ts index 30ec3a642..25fba8bb0 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/span-propagation.ts @@ -248,8 +248,13 @@ export function installFetchSpanPropagation(options: FetchSpanPropagationOptions ? init.headers : (isRequest ? (input as Request).headers : undefined); const headers = new Headers(base); - headers.set(SPAN_CONTEXT_HEADER, encodeSpanContextHeader(context)); - return callFetch(input, { ...init, headers }); + // An explicitly-set header (getSpanPropagationHeaders) always wins + // over the ambient context — it is the caller's precise intent, e.g. + // pinning one request to one span under interleaved async flows. + if (!headers.has(SPAN_CONTEXT_HEADER)) { + headers.set(SPAN_CONTEXT_HEADER, encodeSpanContextHeader(context)); + return callFetch(input, { ...init, headers }); + } } } } diff --git a/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts b/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts index f185b968e..f2a7178c6 100644 --- a/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts +++ b/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts @@ -167,8 +167,14 @@ export type StackClientApp, + getSpanPropagationHeaders(options?: { parentIds?: ParentRef[], root?: boolean }): Record, // note: we don't special-case 'anonymous' here to return non-null, see GetPartialUserOptions for more details getPartialUser(options: GetCurrentPartialUserOptions & { from: 'token' }): Promise,