Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { expect } from '@playwright/test';
import type { ClientReport } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import {
envelopeRequestParser,
hidePage,
shouldSkipTracingTest,
testingCdnBundle,
waitForClientReportRequest,
} from '../../../utils/helpers';
import { shouldSkipTracingTest, testingCdnBundle } from '../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils';

sentryTest(
'records no_parent_span client report for fetch requests without an active span',
'sends http.client span for fetch requests without an active span when span streaming is enabled',
async ({ getLocalTestUrl, page }) => {
sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());

Expand All @@ -24,22 +18,14 @@ sentryTest(

const url = await getLocalTestUrl({ testDir: __dirname });

const clientReportPromise = waitForClientReportRequest(page, report => {
return report.discarded_events.some(e => e.reason === 'no_parent_span');
});
const spanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client');

await page.goto(url);

await hidePage(page);

const clientReport = envelopeRequestParser<ClientReport>(await clientReportPromise);
const span = await spanPromise;

expect(clientReport.discarded_events).toEqual([
{
category: 'span',
quantity: 1,
reason: 'no_parent_span',
},
]);
expect(span.name).toMatch(/^GET /);
expect(span.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser' });
expect(span.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'http.client' });
},
);
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('no_parent_span client report (streaming)', () => {
describe('no_parent_span with streaming enabled', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('records no_parent_span outcome for http.client span without a local parent', async () => {
test('sends http.client span without a local parent when span streaming is enabled', async () => {

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > cjs > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:321:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > esm > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:311:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > cjs > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:321:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > esm > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:311:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (18) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > cjs > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:321:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (18) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > esm > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:311:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (20) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > cjs > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:321:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (20) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > esm > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:311:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) (TS 3.8) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > cjs > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:321:7

Check failure on line 10 in dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) (TS 3.8) Integration Tests

suites/tracing/no-parent-span-client-report-streamed/test.ts > no_parent_span with streaming enabled > esm/cjs > esm > sends http.client span without a local parent when span streaming is enabled

Error: Test timed out in 15000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ suites/tracing/no-parent-span-client-report-streamed/test.ts:10:5 ❯ utils/runner.ts:311:7
const runner = createRunner()
.unignore('client_report')
.expect({
client_report: report => {
expect(report.discarded_events).toEqual([
{
category: 'span',
quantity: 1,
reason: 'no_parent_span',
},
]);
span: span => {
const httpClientSpan = span.items.find(item =>
item.attributes?.['sentry.op']
? item.attributes['sentry.op'].type === 'string' && item.attributes['sentry.op'].value === 'http.client'
: false,
);

expect(httpClientSpan).toBeDefined();
expect(httpClientSpan?.name).toMatch(/^GET /);
},
})
.start();
Expand Down
8 changes: 5 additions & 3 deletions packages/browser/src/tracing/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,11 @@ function xhrCallback(

const client = getClient();
const hasParent = !!getActiveSpan();
// With span streaming, we always emit http.client spans, even without a parent span
const shouldEmitSpan = hasParent || (!!client && hasSpanStreamingEnabled(client));

const span =
shouldCreateSpanResult && hasParent
shouldCreateSpanResult && shouldEmitSpan
? startInactiveSpan({
name: `${method} ${urlForSpanName}`,
attributes: {
Expand All @@ -425,7 +427,7 @@ function xhrCallback(
})
: new SentryNonRecordingSpan();

if (shouldCreateSpanResult && !hasParent) {
if (shouldCreateSpanResult && !shouldEmitSpan) {
client?.recordDroppedEvent('no_parent_span', 'span');
}

Expand All @@ -438,7 +440,7 @@ function xhrCallback(
// If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction),
// we do not want to use the span as base for the trace headers,
// which means that the headers will be generated from the scope and the sampling decision is deferred
hasSpansEnabled() && hasParent ? span : undefined,
hasSpansEnabled() && shouldEmitSpan ? span : undefined,
propagateTraceparent,
);
}
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getClient } from './currentScopes';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes';
import { setHttpStatus, SPAN_STATUS_ERROR, startInactiveSpan } from './tracing';
import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan';
import { hasSpanStreamingEnabled } from './tracing/spans/hasSpanStreamingEnabled';
import type { FetchBreadcrumbHint } from './types-hoist/breadcrumb';
import type { HandlerDataFetch } from './types-hoist/instrument';
import type { ResponseHookInfo } from './types-hoist/request';
Expand Down Expand Up @@ -110,13 +111,15 @@ export function instrumentFetchRequest(

const client = getClient();
const hasParent = !!getActiveSpan();
// With span streaming, we always emit http.client spans, even without a parent span
const shouldEmitSpan = hasParent || (!!client && hasSpanStreamingEnabled(client));

const span =
shouldCreateSpanResult && hasParent
shouldCreateSpanResult && shouldEmitSpan
? startInactiveSpan(getSpanStartOptions(url, method, spanOrigin))
: new SentryNonRecordingSpan();

if (shouldCreateSpanResult && !hasParent) {
if (shouldCreateSpanResult && !shouldEmitSpan) {
client?.recordDroppedEvent('no_parent_span', 'span');
}

Expand All @@ -136,7 +139,7 @@ export function instrumentFetchRequest(
// If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction),
// we do not want to use the span as base for the trace headers,
// which means that the headers will be generated from the scope and the sampling decision is deferred
hasSpansEnabled() && hasParent ? span : undefined,
hasSpansEnabled() && shouldEmitSpan ? span : undefined,
propagateTraceparent,
);
if (headers) {
Expand Down
9 changes: 6 additions & 3 deletions packages/opentelemetry/src/sampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@ export class SentrySampler implements Sampler {
const maybeSpanHttpMethod = spanAttributes[SEMATTRS_HTTP_METHOD] || spanAttributes[ATTR_HTTP_REQUEST_METHOD];

// If we have a http.client span that has no local parent, we never want to sample it
// but we want to leave downstream sampling decisions up to the server
// but we want to leave downstream sampling decisions up to the server.
// Exception: when span streaming is enabled, we always emit these spans.
if (spanKind === SpanKind.CLIENT && maybeSpanHttpMethod && (!parentSpan || parentContext?.isRemote)) {
this._client.recordDroppedEvent('no_parent_span', 'span');
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
if (!this._isSpanStreaming) {
this._client.recordDroppedEvent('no_parent_span', 'span');
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
}
}

const parentSampled = parentSpan ? getParentSampled(parentSpan, traceId, spanName) : undefined;
Expand Down
18 changes: 18 additions & 0 deletions packages/opentelemetry/test/sampler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,5 +348,23 @@ describe('SentrySampler', () => {
expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
expect(spyOnDroppedEvent).toHaveBeenCalledWith('sample_rate', 'span');
});

it('always emits streamed http.client spans without a local parent', () => {
const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream' }));
const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
const sampler = new SentrySampler(client);

const ctx = context.active();
const traceId = generateTraceId();
const spanName = 'GET http://example.com/api';
const spanKind = SpanKind.CLIENT;
const spanAttributes = {
[ATTR_HTTP_REQUEST_METHOD]: 'GET',
};

const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined);
expect(actual.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
expect(spyOnDroppedEvent).not.toHaveBeenCalled();
});
});
});
Loading