Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/web/test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,27 @@ describe('initErrors', () => {
const exc = span.events.find((e) => e.name === 'exception');
expect((exc?.attributes?.['exception.message'] as string).length).toBeLessThanOrEqual(256);
});

it("falls back to 'Error' when the event carries no error object", () => {
// Resource-load failures and some synthetic events fire 'error' with no
// `.error` — the type must degrade gracefully rather than throw.
window.dispatchEvent(new ErrorEvent('error', { message: 'no error object' }));
const [span] = exporter.getFinishedSpans();
const exc = span.events.find((e) => e.name === 'exception');
expect(exc?.attributes?.['exception.type']).toBe('Error');
});

it('strips file paths out of error messages', () => {
window.dispatchEvent(
new ErrorEvent('error', {
message: 'Boom at /src/app/checkout.js:42:7 in handler',
error: new Error(),
}),
);
const [span] = exporter.getFinishedSpans();
const exc = span.events.find((e) => e.name === 'exception');
const msg = exc?.attributes?.['exception.message'] as string;
expect(msg).not.toContain('checkout.js');
expect(msg).toContain('[file]');
});
});
46 changes: 46 additions & 0 deletions packages/web/test/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,52 @@ describe('initFetch — self-instrumentation guard', () => {
});
});

describe('initFetch — header-shape normalization', () => {
it('merges traceparent into a Headers instance without dropping existing entries', async () => {
await window.fetch('/api/data', { headers: new Headers({ 'x-custom': 'keep-me' }) });
const headers = lastInit?.headers as Record<string, string>;
expect(headers['x-custom']).toBe('keep-me');
expect(headers.traceparent).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/);
});

it('merges traceparent into array-of-tuples headers without dropping existing entries', async () => {
await window.fetch('/api/data', { headers: [['x-custom', 'keep-me']] });
const headers = lastInit?.headers as Record<string, string>;
expect(headers['x-custom']).toBe('keep-me');
expect(headers.traceparent).toBeDefined();
});
});

describe('initFetch — input types', () => {
it('instruments a URL-object input', async () => {
await window.fetch(new URL('https://api.example.com/things'));
const [span] = exporter.getFinishedSpans();
expect(span.name).toBe('GET /things');
expect(span.attributes['url.full']).toBe('https://api.example.com/things');
});

it('derives the method from a Request-object input', async () => {
await window.fetch(new Request('/api/orders', { method: 'POST' }));
const [span] = exporter.getFinishedSpans();
expect(span.attributes['http.request.method']).toBe('POST');
expect(span.name).toBe('POST /api/orders');
// The wrapper re-wraps the Request so the injected traceparent rides along.
expect((lastInit?.headers as Record<string, string>).traceparent).toBeDefined();
});
});

describe('initFetch — malformed URL', () => {
it('falls back to the raw URL in the span name when URL parsing throws', async () => {
await window.fetch('http://[malformed');
const [span] = exporter.getFinishedSpans();
// getPathname() and the self-instrumentation guard both swallow the parse
// error; the request is still instrumented using the raw URL string.
expect(span.name).toBe('GET http://[malformed');
expect(span.attributes['url.full']).toBe('http://[malformed');
expect((lastInit?.headers as Record<string, string>).traceparent).toBeDefined();
});
});

describe('initFetch — privacy invariants', () => {
it('does not capture request body or authorization headers', async () => {
await window.fetch('/api/login', {
Expand Down