Skip to content

Commit 28000c2

Browse files
authored
Merge branch 'develop' into nh/shared-finalize-stream-span
2 parents a17f58a + 8ffd1de commit 28000c2

File tree

102 files changed

+3146
-1220
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+3146
-1220
lines changed

.github/workflows/create-issue-for-unreferenced-prs.yml

Lines changed: 0 additions & 130 deletions
This file was deleted.

.github/workflows/validate-pr.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Validate PR
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened]
6+
7+
jobs:
8+
validate-pr:
9+
runs-on: ubuntu-24.04
10+
permissions:
11+
pull-requests: write
12+
steps:
13+
- uses: getsentry/github-workflows/validate-pr@0b52fc6a867b744dcbdf5d25c18bc8d1c95710e1
14+
with:
15+
app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }}
16+
private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }}

CHANGELOG.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,33 @@
44

55
### Important Changes
66

7+
- **feat(node-core): Add OTLP integration for node-core/light ([#19729](https://github.com/getsentry/sentry-javascript/pull/19729))**
8+
9+
Added `otlpIntegration` at `@sentry/node-core/light/otlp` for users who manage
10+
their own OpenTelemetry setup and want to send trace data to Sentry without
11+
adopting the full `@sentry/node` SDK.
12+
13+
```js
14+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
15+
import * as Sentry from '@sentry/node-core/light';
16+
import { otlpIntegration } from '@sentry/node-core/light/otlp';
17+
18+
const provider = new NodeTracerProvider();
19+
provider.register();
20+
21+
Sentry.init({
22+
dsn: '__DSN__',
23+
integrations: [
24+
otlpIntegration({
25+
// Export OTel spans to Sentry via OTLP (default: true)
26+
setupOtlpTracesExporter: true,
27+
}),
28+
],
29+
});
30+
```
31+
32+
The integration links Sentry errors to OTel traces and exports spans to Sentry via OTLP.
33+
734
- **feat(node, bun): Add runtime metrics integrations for Node.js and Bun ([#19923](https://github.com/getsentry/sentry-javascript/pull/19923), [#19979](https://github.com/getsentry/sentry-javascript/pull/19979))**
835

936
New `nodeRuntimeMetricsIntegration` and `bunRuntimeMetricsIntegration` automatically collect runtime health metrics and send them to Sentry on a configurable interval (default: 30s). Collected metrics include memory (RSS, heap used/total), CPU utilization, event loop utilization, and process uptime. Node additionally collects event loop delay percentiles (p50, p99). Extra metrics like CPU time and external memory are available as opt-in.
@@ -31,6 +58,21 @@
3158

3259
Adds instrumentation for the Google GenAI [`embedContent`](https://ai.google.dev/gemini-api/docs/embeddings) API, creating `gen_ai.embeddings` spans.
3360

61+
- **ref(core): Remove provider-specific AI span attributes in favor of `gen_ai` attributes in sentry conventions ([#20011](https://github.com/getsentry/sentry-javascript/pull/20011))**
62+
63+
The following provider-specific span attributes have been removed from the OpenAI and Anthropic AI integrations. Use the standardized `gen_ai.*` equivalents instead:
64+
65+
| Removed attribute | Replacement |
66+
| -------------------------------- | ---------------------------- |
67+
| `openai.response.id` | `gen_ai.response.id` |
68+
| `openai.response.model` | `gen_ai.response.model` |
69+
| `openai.usage.prompt_tokens` | `gen_ai.usage.input_tokens` |
70+
| `openai.usage.completion_tokens` | `gen_ai.usage.output_tokens` |
71+
| `openai.response.timestamp` | _(removed, no replacement)_ |
72+
| `anthropic.response.timestamp` | _(removed, no replacement)_ |
73+
74+
If you reference these attributes in hooks (e.g. `beforeSendTransaction`), update them to the `gen_ai.*` equivalents.
75+
3476
## 10.46.0
3577

3678
### Important Changes

dev-packages/browser-integration-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@sentry-internal/rrweb": "2.34.0",
6363
"@sentry/browser": "10.46.0",
6464
"@supabase/supabase-js": "2.49.3",
65-
"axios": "^1.12.2",
65+
"axios": "1.13.5",
6666
"babel-loader": "^10.1.1",
6767
"fflate": "0.8.2",
6868
"html-webpack-plugin": "^5.5.0",

dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts

Lines changed: 37 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,15 @@
1-
import type { Page, Request, Route } from '@playwright/test';
1+
import type { Page, Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { Envelope } from '@sentry/core';
3+
import type { SerializedMetric } from '@sentry/core';
44
import { sentryTest } from '../../../../utils/fixtures';
5-
import {
6-
properFullEnvelopeRequestParser,
7-
shouldSkipMetricsTest,
8-
shouldSkipTracingTest,
9-
} from '../../../../utils/helpers';
10-
11-
type MetricItem = Record<string, unknown> & {
12-
name: string;
13-
type: string;
14-
value: number;
15-
unit?: string;
16-
attributes: Record<string, { value: string | number; type: string }>;
17-
};
18-
19-
function extractMetricsFromRequest(req: Request): MetricItem[] {
20-
try {
21-
const envelope = properFullEnvelopeRequestParser<Envelope>(req);
22-
const items = envelope[1];
23-
const metrics: MetricItem[] = [];
24-
for (const item of items) {
25-
const [header] = item;
26-
if (header.type === 'trace_metric') {
27-
const payload = item[1] as { items?: MetricItem[] };
28-
if (payload.items) {
29-
metrics.push(...payload.items);
30-
}
31-
}
32-
}
33-
return metrics;
34-
} catch {
35-
return [];
36-
}
37-
}
38-
39-
/**
40-
* Collects element timing metrics from envelope requests on the page.
41-
* Returns a function to get all collected metrics so far and a function
42-
* that waits until all expected identifiers have been seen in render_time metrics.
43-
*/
44-
function createMetricCollector(page: Page) {
45-
const collectedRequests: Request[] = [];
46-
47-
page.on('request', req => {
48-
if (!req.url().includes('/api/1337/envelope/')) return;
49-
const metrics = extractMetricsFromRequest(req);
50-
if (metrics.some(m => m.name.startsWith('ui.element.'))) {
51-
collectedRequests.push(req);
52-
}
53-
});
54-
55-
function getAll(): MetricItem[] {
56-
return collectedRequests.flatMap(req => extractMetricsFromRequest(req));
57-
}
5+
import { shouldSkipMetricsTest, shouldSkipTracingTest, waitForMetrics } from '../../../../utils/helpers';
586

59-
async function waitForIdentifiers(identifiers: string[], timeout = 30_000): Promise<void> {
60-
const deadline = Date.now() + timeout;
61-
while (Date.now() < deadline) {
62-
const all = getAll().filter(m => m.name === 'ui.element.render_time');
63-
const seen = new Set(all.map(m => m.attributes['ui.element.identifier']?.value));
64-
if (identifiers.every(id => seen.has(id))) {
65-
return;
66-
}
67-
await page.waitForTimeout(500);
68-
}
69-
// Final check with assertion for clear error message
70-
const all = getAll().filter(m => m.name === 'ui.element.render_time');
71-
const seen = all.map(m => m.attributes['ui.element.identifier']?.value);
72-
for (const id of identifiers) {
73-
expect(seen).toContain(id);
74-
}
75-
}
76-
77-
function reset(): void {
78-
collectedRequests.length = 0;
79-
}
7+
function getIdentifier(m: SerializedMetric): unknown {
8+
return m.attributes?.['ui.element.identifier']?.value;
9+
}
8010

81-
return { getAll, waitForIdentifiers, reset };
11+
function getPaintType(m: SerializedMetric): unknown {
12+
return m.attributes?.['ui.element.paint_type']?.value;
8213
}
8314

8415
sentryTest(
@@ -91,19 +22,23 @@ sentryTest(
9122
serveAssets(page);
9223

9324
const url = await getLocalTestUrl({ testDir: __dirname });
94-
const collector = createMetricCollector(page);
9525

96-
await page.goto(url);
26+
const expectedIdentifiers = ['image-fast', 'text1', 'button1', 'image-slow', 'lazy-image', 'lazy-text'];
9727

98-
// Wait until all expected element identifiers have been flushed as metrics
99-
await collector.waitForIdentifiers(['image-fast', 'text1', 'button1', 'image-slow', 'lazy-image', 'lazy-text']);
28+
// Wait for all expected element identifiers to arrive as metrics
29+
const [allMetrics] = await Promise.all([
30+
waitForMetrics(page, metrics => {
31+
const seen = new Set(metrics.filter(m => m.name === 'ui.element.render_time').map(getIdentifier));
32+
return expectedIdentifiers.every(id => seen.has(id));
33+
}),
34+
page.goto(url),
35+
]);
10036

101-
const allMetrics = collector.getAll().filter(m => m.name.startsWith('ui.element.'));
10237
const renderTimeMetrics = allMetrics.filter(m => m.name === 'ui.element.render_time');
10338
const loadTimeMetrics = allMetrics.filter(m => m.name === 'ui.element.load_time');
10439

105-
const renderIdentifiers = renderTimeMetrics.map(m => m.attributes['ui.element.identifier']?.value);
106-
const loadIdentifiers = loadTimeMetrics.map(m => m.attributes['ui.element.identifier']?.value);
40+
const renderIdentifiers = renderTimeMetrics.map(getIdentifier);
41+
const loadIdentifiers = loadTimeMetrics.map(getIdentifier);
10742

10843
// All text and image elements should have render_time
10944
expect(renderIdentifiers).toContain('image-fast');
@@ -124,18 +59,18 @@ sentryTest(
12459
expect(loadIdentifiers).not.toContain('lazy-text');
12560

12661
// Validate metric structure for image-fast
127-
const imageFastRender = renderTimeMetrics.find(m => m.attributes['ui.element.identifier']?.value === 'image-fast');
62+
const imageFastRender = renderTimeMetrics.find(m => getIdentifier(m) === 'image-fast');
12863
expect(imageFastRender).toMatchObject({
12964
name: 'ui.element.render_time',
13065
type: 'distribution',
13166
unit: 'millisecond',
13267
value: expect.any(Number),
13368
});
134-
expect(imageFastRender!.attributes['ui.element.paint_type']?.value).toBe('image-paint');
69+
expect(getPaintType(imageFastRender!)).toBe('image-paint');
13570

13671
// Validate text-paint metric
137-
const text1Render = renderTimeMetrics.find(m => m.attributes['ui.element.identifier']?.value === 'text1');
138-
expect(text1Render!.attributes['ui.element.paint_type']?.value).toBe('text-paint');
72+
const text1Render = renderTimeMetrics.find(m => getIdentifier(m) === 'text1');
73+
expect(getPaintType(text1Render!)).toBe('text-paint');
13974
},
14075
);
14176

@@ -147,25 +82,31 @@ sentryTest('emits element timing metrics after navigation', async ({ getLocalTes
14782
serveAssets(page);
14883

14984
const url = await getLocalTestUrl({ testDir: __dirname });
150-
const collector = createMetricCollector(page);
85+
86+
// Start listening before navigation to avoid missing metrics
87+
const pageloadMetricsPromise = waitForMetrics(page, metrics =>
88+
metrics.some(m => m.name === 'ui.element.render_time' && getIdentifier(m) === 'image-fast'),
89+
);
15190

15291
await page.goto(url);
15392

15493
// Wait for pageload element timing metrics to arrive before navigating
155-
await collector.waitForIdentifiers(['image-fast', 'text1']);
94+
await pageloadMetricsPromise;
15695

157-
// Reset so we only capture post-navigation metrics
158-
collector.reset();
96+
// Start listening before click to avoid missing metrics
97+
const navigationMetricsPromise = waitForMetrics(page, metrics => {
98+
const seen = new Set(metrics.filter(m => m.name === 'ui.element.render_time').map(getIdentifier));
99+
return seen.has('navigation-image') && seen.has('navigation-text');
100+
});
159101

160102
// Trigger navigation
161103
await page.locator('#button1').click();
162104

163105
// Wait for navigation element timing metrics
164-
await collector.waitForIdentifiers(['navigation-image', 'navigation-text']);
106+
const navigationMetrics = await navigationMetricsPromise;
165107

166-
const allMetrics = collector.getAll();
167-
const renderTimeMetrics = allMetrics.filter(m => m.name === 'ui.element.render_time');
168-
const renderIdentifiers = renderTimeMetrics.map(m => m.attributes['ui.element.identifier']?.value);
108+
const renderTimeMetrics = navigationMetrics.filter(m => m.name === 'ui.element.render_time');
109+
const renderIdentifiers = renderTimeMetrics.map(getIdentifier);
169110

170111
expect(renderIdentifiers).toContain('navigation-image');
171112
expect(renderIdentifiers).toContain('navigation-text');

0 commit comments

Comments
 (0)