Skip to content

Commit c3a9682

Browse files
authored
feat(core): Add orgId option to init and DSC (sentry-org_id in baggage) (#16305)
Adds the organization ID to the DSC (dynamic sampling context). This will add the org ID as `sentry-org_id` to the baggage. This org ID is parsed from the DSN. With the `orgId` option it's possible to overwrite the automatically parsed organization ID. closes #16290
1 parent 6d63705 commit c3a9682

File tree

12 files changed

+343
-5
lines changed

12 files changed

+343
-5
lines changed

.size-limit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = [
88
path: 'packages/browser/build/npm/esm/index.js',
99
import: createImport('init'),
1010
gzip: true,
11-
limit: '24 KB',
11+
limit: '25 KB',
1212
},
1313
{
1414
name: '@sentry/browser - with treeshaking flags',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
4+
export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
5+
6+
Sentry.init({
7+
dsn: 'https://public@o01234987.ingest.sentry.io/1337',
8+
release: '1.0',
9+
environment: 'prod',
10+
tracesSampleRate: 1.0,
11+
transport: loggingTransport,
12+
});
13+
14+
import cors from 'cors';
15+
import express from 'express';
16+
import * as http from 'http';
17+
18+
const app = express();
19+
20+
app.use(cors());
21+
22+
app.get('/test/express', (_req, res) => {
23+
const headers = http
24+
.get({
25+
hostname: 'example.com',
26+
})
27+
.getHeaders();
28+
29+
res.send({ test_data: headers });
30+
});
31+
32+
Sentry.setupExpressErrorHandler(app);
33+
34+
startExpressServerAndSendPortToRunner(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
4+
export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
5+
6+
Sentry.init({
7+
dsn: 'https://public@public.ingest.sentry.io/1337',
8+
release: '1.0',
9+
environment: 'prod',
10+
tracesSampleRate: 1.0,
11+
transport: loggingTransport,
12+
});
13+
14+
import cors from 'cors';
15+
import express from 'express';
16+
import * as http from 'http';
17+
18+
const app = express();
19+
20+
app.use(cors());
21+
22+
app.get('/test/express', (_req, res) => {
23+
const headers = http
24+
.get({
25+
hostname: 'example.com',
26+
})
27+
.getHeaders();
28+
29+
res.send({ test_data: headers });
30+
});
31+
32+
Sentry.setupExpressErrorHandler(app);
33+
34+
startExpressServerAndSendPortToRunner(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
4+
export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
5+
6+
Sentry.init({
7+
dsn: 'https://public@o0000987.ingest.sentry.io/1337',
8+
release: '1.0',
9+
environment: 'prod',
10+
orgId: '01234987',
11+
tracesSampleRate: 1.0,
12+
transport: loggingTransport,
13+
});
14+
15+
import cors from 'cors';
16+
import express from 'express';
17+
import * as http from 'http';
18+
19+
const app = express();
20+
21+
app.use(cors());
22+
23+
app.get('/test/express', (_req, res) => {
24+
const headers = http
25+
.get({
26+
hostname: 'example.com',
27+
})
28+
.getHeaders();
29+
30+
res.send({ test_data: headers });
31+
});
32+
33+
Sentry.setupExpressErrorHandler(app);
34+
35+
startExpressServerAndSendPortToRunner(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { afterAll, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
import type { TestAPIResponse } from './server';
4+
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
test('should include explicitly set org_id in the baggage header', async () => {
10+
const runner = createRunner(__dirname, 'server.ts').start();
11+
12+
const response = await runner.makeRequest<TestAPIResponse>('get', '/test/express');
13+
expect(response).toBeDefined();
14+
15+
const baggage = response?.test_data.baggage;
16+
expect(baggage).toContain('sentry-org_id=01234987');
17+
});
18+
19+
test('should extract org_id from DSN host when not explicitly set', async () => {
20+
const runner = createRunner(__dirname, 'server-no-explicit-org-id.ts').start();
21+
22+
const response = await runner.makeRequest<TestAPIResponse>('get', '/test/express');
23+
expect(response).toBeDefined();
24+
25+
const baggage = response?.test_data.baggage;
26+
expect(baggage).toContain('sentry-org_id=01234987');
27+
});
28+
29+
test('should set undefined org_id when it cannot be extracted', async () => {
30+
const runner = createRunner(__dirname, 'server-no-org-id.ts').start();
31+
32+
const response = await runner.makeRequest<TestAPIResponse>('get', '/test/express');
33+
expect(response).toBeDefined();
34+
35+
const baggage = response?.test_data.baggage;
36+
expect(baggage).not.toContain('sentry-org_id');
37+
});

packages/browser/test/tracing/browserTracingIntegration.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,7 @@ describe('browserTracingIntegration', () => {
732732
sampleRand: expect.any(Number),
733733
dsc: {
734734
release: undefined,
735+
org_id: undefined,
735736
environment: 'production',
736737
public_key: 'examplePublicKey',
737738
sample_rate: '1',
@@ -773,6 +774,7 @@ describe('browserTracingIntegration', () => {
773774
sampleRand: expect.any(Number),
774775
dsc: {
775776
release: undefined,
777+
org_id: undefined,
776778
environment: 'production',
777779
public_key: 'examplePublicKey',
778780
sample_rate: '0',
@@ -898,6 +900,7 @@ describe('browserTracingIntegration', () => {
898900
expect(dynamicSamplingContext).toBeDefined();
899901
expect(dynamicSamplingContext).toStrictEqual({
900902
release: undefined,
903+
org_id: undefined,
901904
environment: 'production',
902905
public_key: 'examplePublicKey',
903906
sample_rate: '1',

packages/core/src/tracing/dynamicSamplingContext.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
baggageHeaderToDynamicSamplingContext,
1616
dynamicSamplingContextToSentryBaggageHeader,
1717
} from '../utils-hoist/baggage';
18+
import { extractOrgIdFromDsnHost } from '../utils-hoist/dsn';
1819
import { addNonEnumerableProperty } from '../utils-hoist/object';
1920
import { getCapturedScopesOnSpan } from './utils';
2021

@@ -44,7 +45,14 @@ export function freezeDscOnSpan(span: Span, dsc: Partial<DynamicSamplingContext>
4445
export function getDynamicSamplingContextFromClient(trace_id: string, client: Client): DynamicSamplingContext {
4546
const options = client.getOptions();
4647

47-
const { publicKey: public_key } = client.getDsn() || {};
48+
const { publicKey: public_key, host } = client.getDsn() || {};
49+
50+
let org_id: string | undefined;
51+
if (options.orgId) {
52+
org_id = String(options.orgId);
53+
} else if (host) {
54+
org_id = extractOrgIdFromDsnHost(host);
55+
}
4856

4957
// Instead of conditionally adding non-undefined values, we add them and then remove them if needed
5058
// otherwise, the order of baggage entries changes, which "breaks" a bunch of tests etc.
@@ -53,6 +61,7 @@ export function getDynamicSamplingContextFromClient(trace_id: string, client: Cl
5361
release: options.release,
5462
public_key,
5563
trace_id,
64+
org_id,
5665
};
5766

5867
client.emit('createDsc', dsc);

packages/core/src/types-hoist/envelope.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type DynamicSamplingContext = {
2525
replay_id?: string;
2626
sampled?: string;
2727
sample_rand?: string;
28+
org_id?: string;
2829
};
2930

3031
// https://github.com/getsentry/relay/blob/311b237cd4471042352fa45e7a0824b8995f216f/relay-server/src/envelope.rs#L154

packages/core/src/types-hoist/options.ts

+8
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,14 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
320320
*/
321321
tracePropagationTargets?: TracePropagationTargets;
322322

323+
/**
324+
* The organization ID of the current SDK. The organization ID is a string containing only numbers. This ID is used to
325+
* propagate traces to other Sentry services.
326+
*
327+
* The SDK tries to automatically extract the organization ID from the DSN. With this option, you can override it.
328+
*/
329+
orgId?: `${number}` | number;
330+
323331
/**
324332
* Function to compute tracing sample rate dynamically and filter unwanted traces.
325333
*

packages/core/src/utils-hoist/dsn.ts

+15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { DsnComponents, DsnLike, DsnProtocol } from '../types-hoist/dsn';
22
import { DEBUG_BUILD } from './../debug-build';
33
import { consoleSandbox, logger } from './logger';
44

5+
/** Regular expression used to extract org ID from a DSN host. */
6+
const ORG_ID_REGEX = /^o(\d+)\./;
7+
58
/** Regular expression used to parse a Dsn. */
69
const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+)?)?@)([\w.-]+)(?::(\d+))?\/(.+)/;
710

@@ -114,6 +117,18 @@ function validateDsn(dsn: DsnComponents): boolean {
114117
return true;
115118
}
116119

120+
/**
121+
* Extract the org ID from a DSN host.
122+
*
123+
* @param host The host from a DSN
124+
* @returns The org ID if found, undefined otherwise
125+
*/
126+
export function extractOrgIdFromDsnHost(host: string): string | undefined {
127+
const match = host.match(ORG_ID_REGEX);
128+
129+
return match?.[1];
130+
}
131+
117132
/**
118133
* Creates a valid Sentry Dsn object, identifying a Sentry instance and project.
119134
* @returns a valid DsnComponents object or `undefined` if @param from is an invalid DSN source

0 commit comments

Comments
 (0)