Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
_enableTraceMetrics: true,
release: '1.0.0',
environment: 'test',
autoSessionTracking: false, // Was causing session envelopes to be sent
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } });
Sentry.metrics.gauge('test.gauge', 42, { unit: 'millisecond', attributes: { server: 'test-1' } });
Sentry.metrics.distribution('test.distribution', 200, { unit: 'second', attributes: { priority: 'high' } });

Sentry.startSpan({ name: 'test-span', op: 'test' }, () => {
Sentry.metrics.count('test.span.counter', 1, { attributes: { operation: 'test' } });
});

Sentry.setUser({ id: 'user-123', email: '[email protected]', username: 'testuser' });
Sentry.metrics.count('test.user.counter', 1, { attributes: { action: 'click' } });

Sentry.flush();
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect } from '@playwright/test';
import type { MetricEnvelope } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../../utils/helpers';

sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) => {
const bundle = process.env.PW_BUNDLE || '';
if (bundle.startsWith('bundle') || bundle.startsWith('loader')) {
sentryTest.skip();
}

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

const event = await getFirstSentryEnvelopeRequest<MetricEnvelope>(page, url, properFullEnvelopeRequestParser);
const envelopeItems = event[1];

expect(envelopeItems[0]).toEqual([
{
type: 'metric',
item_count: 5,
content_type: 'application/vnd.sentry.items.trace-metric+json',
},
{
items: [
{
timestamp: expect.any(Number),
trace_id: expect.any(String),
name: 'test.counter',
type: 'counter',
value: 1,
attributes: {
endpoint: { value: '/api/test', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
},
},
{
timestamp: expect.any(Number),
trace_id: expect.any(String),
name: 'test.gauge',
type: 'gauge',
unit: 'millisecond',
value: 42,
attributes: {
server: { value: 'test-1', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
},
},
{
timestamp: expect.any(Number),
trace_id: expect.any(String),
name: 'test.distribution',
type: 'distribution',
unit: 'second',
value: 200,
attributes: {
priority: { value: 'high', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
},
},
{
timestamp: expect.any(Number),
trace_id: expect.any(String),
span_id: expect.any(String),
name: 'test.span.counter',
type: 'counter',
value: 1,
attributes: {
operation: { value: 'test', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
},
},
{
timestamp: expect.any(Number),
trace_id: expect.any(String),
name: 'test.user.counter',
type: 'counter',
value: 1,
attributes: {
action: { value: 'click', type: 'string' },
'user.id': { value: 'user-123', type: 'string' },
'user.email': { value: '[email protected]', type: 'string' },
'user.name': { value: 'testuser', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
},
},
],
},
]);
});
10 changes: 10 additions & 0 deletions dev-packages/node-integration-tests/utils/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Event,
SerializedCheckIn,
SerializedLogContainer,
SerializedMetricContainer,
SerializedSession,
SessionAggregates,
TransactionEvent,
Expand Down Expand Up @@ -76,6 +77,15 @@ export function assertSentryLogContainer(
});
}

export function assertSentryMetricContainer(
actual: SerializedMetricContainer,
expected: Partial<SerializedMetricContainer>,
): void {
expect(actual).toMatchObject({
...expected,
});
}

export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial<Envelope[0]>): void {
expect(actual).toEqual({
event_id: expect.any(String),
Expand Down
22 changes: 22 additions & 0 deletions dev-packages/node-integration-tests/utils/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
EventEnvelope,
SerializedCheckIn,
SerializedLogContainer,
SerializedMetricContainer,
SerializedSession,
SessionAggregates,
TransactionEvent,
Expand All @@ -25,6 +26,7 @@ import {
assertSentryClientReport,
assertSentryEvent,
assertSentryLogContainer,
assertSentryMetricContainer,
assertSentrySession,
assertSentrySessions,
assertSentryTransaction,
Expand Down Expand Up @@ -130,6 +132,7 @@ type ExpectedSessions = Partial<SessionAggregates> | ((event: SessionAggregates)
type ExpectedCheckIn = Partial<SerializedCheckIn> | ((event: SerializedCheckIn) => void);
type ExpectedClientReport = Partial<ClientReport> | ((event: ClientReport) => void);
type ExpectedLogContainer = Partial<SerializedLogContainer> | ((event: SerializedLogContainer) => void);
type ExpectedMetricContainer = Partial<SerializedMetricContainer> | ((event: SerializedMetricContainer) => void);

type Expected =
| {
Expand All @@ -152,6 +155,9 @@ type Expected =
}
| {
log: ExpectedLogContainer;
}
| {
metric: ExpectedMetricContainer;
};

type ExpectedEnvelopeHeader =
Expand Down Expand Up @@ -380,6 +386,11 @@ export function createRunner(...paths: string[]) {
expectedEnvelopeHeaders.push(expected);
return this;
},
expectMetricEnvelope: function () {
// Unignore metric envelopes
ignored.delete('metric');
return this;
},
withEnv: function (env: Record<string, string>) {
withEnv = env;
return this;
Expand Down Expand Up @@ -514,6 +525,9 @@ export function createRunner(...paths: string[]) {
} else if ('log' in expected) {
expectLog(item[1] as SerializedLogContainer, expected.log);
expectCallbackCalled();
} else if ('metric' in expected) {
expectMetric(item[1] as SerializedMetricContainer, expected.metric);
expectCallbackCalled();
} else {
throw new Error(
`Unhandled expected envelope item type: ${JSON.stringify(expected)}\nItem: ${JSON.stringify(item)}`,
Expand Down Expand Up @@ -769,6 +783,14 @@ function expectLog(item: SerializedLogContainer, expected: ExpectedLogContainer)
}
}

function expectMetric(item: SerializedMetricContainer, expected: ExpectedMetricContainer): void {
if (typeof expected === 'function') {
expected(item);
} else {
assertSentryMetricContainer(item, expected);
}
}

/**
* Converts ESM import statements to CommonJS require statements
* @param content The content of an ESM file
Expand Down
25 changes: 23 additions & 2 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from '@sentry/core';
import {
_INTERNAL_flushLogsBuffer,
_INTERNAL_flushMetricsBuffer,
addAutoIpAddressToSession,
applySdkMetadata,
Client,
Expand Down Expand Up @@ -85,6 +86,7 @@ export type BrowserClientOptions = ClientOptions<BrowserTransportOptions> & Brow
*/
export class BrowserClient extends Client<BrowserClientOptions> {
private _logFlushIdleTimeout: ReturnType<typeof setTimeout> | undefined;
private _metricFlushIdleTimeout: ReturnType<typeof setTimeout> | undefined;
/**
* Creates a new Browser SDK instance.
*
Expand All @@ -106,9 +108,9 @@ export class BrowserClient extends Client<BrowserClientOptions> {

super(opts);

const { sendDefaultPii, sendClientReports, enableLogs } = this._options;
const { sendDefaultPii, sendClientReports, enableLogs, _enableTraceMetrics } = this._options;

if (WINDOW.document && (sendClientReports || enableLogs)) {
if (WINDOW.document && (sendClientReports || enableLogs || _enableTraceMetrics)) {
WINDOW.document.addEventListener('visibilitychange', () => {
if (WINDOW.document.visibilityState === 'hidden') {
if (sendClientReports) {
Expand All @@ -117,6 +119,9 @@ export class BrowserClient extends Client<BrowserClientOptions> {
if (enableLogs) {
_INTERNAL_flushLogsBuffer(this);
}
if (_enableTraceMetrics) {
_INTERNAL_flushMetricsBuffer(this);
}
}
});
}
Expand All @@ -137,6 +142,22 @@ export class BrowserClient extends Client<BrowserClientOptions> {
});
}

if (_enableTraceMetrics) {
this.on('flush', () => {
_INTERNAL_flushMetricsBuffer(this);
});

this.on('afterCaptureMetric', () => {
if (this._metricFlushIdleTimeout) {
clearTimeout(this._metricFlushIdleTimeout);
}

this._metricFlushIdleTimeout = setTimeout(() => {
_INTERNAL_flushMetricsBuffer(this);
}, DEFAULT_FLUSH_INTERVAL);
});
}

if (sendDefaultPii) {
this.on('beforeSendSession', addAutoIpAddressToSession);
}
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export {
spanToTraceHeader,
spanToBaggageHeader,
updateSpanName,
metrics,
} from '@sentry/core';

export {
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/carrier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AsyncContextStrategy } from './asyncContext/types';
import type { Client } from './client';
import type { Scope } from './scope';
import type { SerializedLog } from './types-hoist/log';
import type { SerializedMetric } from './types-hoist/metric';
import { SDK_VERSION } from './utils/version';
import { GLOBAL_OBJ } from './utils/worldwide';

Expand Down Expand Up @@ -32,6 +33,12 @@ export interface SentryCarrier {
*/
clientToLogBufferMap?: WeakMap<Client, Array<SerializedLog>>;

/**
* A map of Sentry clients to their metric buffers.
* This is used to store metrics that are sent to Sentry.
*/
clientToMetricBufferMap?: WeakMap<Client, Array<SerializedMetric>>;

/** Overwrites TextEncoder used in `@sentry/core`, need for `[email protected]` and older */
encodePolyfill?: (input: string) => Uint8Array;
/** Overwrites TextDecoder used in `@sentry/core`, need for `[email protected]` and older */
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { EventProcessor } from './types-hoist/eventprocessor';
import type { FeedbackEvent } from './types-hoist/feedback';
import type { Integration } from './types-hoist/integration';
import type { Log } from './types-hoist/log';
import type { Metric } from './types-hoist/metric';
import type { ClientOptions } from './types-hoist/options';
import type { ParameterizedString } from './types-hoist/parameterize';
import type { RequestEventData } from './types-hoist/request';
Expand Down Expand Up @@ -688,6 +689,20 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'flushLogs', callback: () => void): () => void;

/**
* A hook that is called after capturing a metric. This hooks runs after `beforeSendMetric` is fired.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'afterCaptureMetric', callback: (metric: Metric) => void): () => void;

/**
* A hook that is called when the client is flushing metrics
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'flushMetrics', callback: () => void): () => void;

/**
* A hook that is called when a http server request is started.
* This hook is called after request isolation, but before the request is processed.
Expand Down Expand Up @@ -887,6 +902,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'flushLogs'): void;

/**
* Emit a hook event for client after capturing a metric.
*/
public emit(hook: 'afterCaptureMetric', metric: Metric): void;

/**
* Emit a hook event for client flush metrics
*/
public emit(hook: 'flushMetrics'): void;

/**
* Emit a hook event for client when a http server request is started.
* This hook is called after request isolation, but before the request is processed.
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export type { ReportDialogOptions } from './report-dialog';
export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/internal';
export * as logger from './logs/public-api';
export { consoleLoggingIntegration } from './logs/console-integration';
export {
_INTERNAL_captureMetric,
_INTERNAL_flushMetricsBuffer,
_INTERNAL_captureSerializedMetric,
} from './metrics/internal';
export * as metrics from './metrics/public-api';
export type { MetricOptions } from './metrics/public-api';
export { createConsolaReporter } from './integrations/consola';
export { addVercelAiProcessors } from './utils/vercel-ai';
export { _INTERNAL_getSpanForToolCallId, _INTERNAL_cleanupToolCallSpan } from './utils/vercel-ai/utils';
Expand Down Expand Up @@ -355,6 +362,7 @@ export type {
SpanEnvelope,
SpanItem,
LogEnvelope,
MetricEnvelope,
} from './types-hoist/envelope';
export type { ExtendedError } from './types-hoist/error';
export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './types-hoist/event';
Expand Down Expand Up @@ -416,6 +424,13 @@ export type {
} from './types-hoist/span';
export type { SpanStatus } from './types-hoist/spanStatus';
export type { Log, LogSeverityLevel } from './types-hoist/log';
export type {
Metric,
MetricType,
SerializedMetric,
SerializedMetricContainer,
SerializedMetricAttributeValue,
} from './types-hoist/metric';
export type { TimedEvent } from './types-hoist/timedEvent';
export type { StackFrame } from './types-hoist/stackframe';
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './types-hoist/stacktrace';
Expand Down
Loading
Loading