diff --git a/.size-limit.js b/.size-limit.js index 08da6f5ce85b..5ccf34d416c0 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -38,7 +38,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '40.7 KB', + limit: '41 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -82,7 +82,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '96 KB', + limit: '97 KB', }, { name: '@sentry/browser (incl. Feedback)', @@ -128,7 +128,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '29 KB', + limit: '30 KB', }, { name: '@sentry/vue (incl. Tracing)', diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index af7a1d6ee2ec..1b4289d66992 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -26,8 +26,6 @@ import type { BrowserTransportOptions } from './transports/types'; */ declare const __SENTRY_RELEASE__: string | undefined; -const DEFAULT_FLUSH_INTERVAL = 5000; - type BrowserSpecificOptions = BrowserClientReplayOptions & BrowserClientProfilingOptions & { /** If configured, this URL will be used as base URL for lazy loading integration. */ @@ -85,8 +83,6 @@ export type BrowserClientOptions = ClientOptions & Brow * @see SentryClient for usage documentation. */ export class BrowserClient extends Client { - private _logFlushIdleTimeout: ReturnType | undefined; - private _metricFlushIdleTimeout: ReturnType | undefined; /** * Creates a new Browser SDK instance. * @@ -110,6 +106,7 @@ export class BrowserClient extends Client { const { sendDefaultPii, sendClientReports, enableLogs, _experiments } = this._options; + // Flush logs and metrics when page becomes hidden (e.g., tab switch, navigation) if (WINDOW.document && (sendClientReports || enableLogs || _experiments?.enableMetrics)) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { @@ -126,38 +123,6 @@ export class BrowserClient extends Client { }); } - if (enableLogs) { - this.on('flush', () => { - _INTERNAL_flushLogsBuffer(this); - }); - - this.on('afterCaptureLog', () => { - if (this._logFlushIdleTimeout) { - clearTimeout(this._logFlushIdleTimeout); - } - - this._logFlushIdleTimeout = setTimeout(() => { - _INTERNAL_flushLogsBuffer(this); - }, DEFAULT_FLUSH_INTERVAL); - }); - } - - if (_experiments?.enableMetrics) { - 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); } diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index c1fcac17444b..d99e45984f0a 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -18,7 +18,6 @@ vi.mock('@sentry/core', async requireActual => { describe('BrowserClient', () => { let client: BrowserClient; - const DEFAULT_FLUSH_INTERVAL = 5000; afterEach(() => { vi.useRealTimers(); @@ -77,59 +76,6 @@ describe('BrowserClient', () => { expect(flushOutcomesSpy).toHaveBeenCalled(); expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); }); - - it('flushes logs on flush event', () => { - const scope = new Scope(); - scope.setClient(client); - - // Add some logs - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, scope); - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, scope); - - // Trigger flush event - client.emit('flush'); - - expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); - }); - - it('flushes logs after idle timeout', () => { - const scope = new Scope(); - scope.setClient(client); - - // Add a log which will trigger afterCaptureLog event - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log' }, scope); - - // Fast forward the idle timeout - vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL); - - expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); - }); - - it('resets idle timeout when new logs are captured', () => { - const scope = new Scope(); - scope.setClient(client); - - // Add initial log - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, scope); - - // Fast forward part of the idle timeout - vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL / 2); - - // Add another log which should reset the timeout - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, scope); - - // Fast forward the remaining time - vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL / 2); - - // Should not have flushed yet since timeout was reset - expect(sentryCore._INTERNAL_flushLogsBuffer).not.toHaveBeenCalled(); - - // Fast forward the full timeout - vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL); - - // Now should have flushed both logs - expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); - }); }); }); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index de6c5f9f1119..6a269a969c8d 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,21 +1,19 @@ /* eslint-disable max-lines */ import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { DEFAULT_ENVIRONMENT } from './constants'; -import { getCurrentScope, getIsolationScope, getTraceContextFromScope, withScope } from './currentScopes'; +import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import type { IntegrationIndex } from './integration'; import { afterSetupIntegrations, setupIntegration, setupIntegrations } from './integration'; +import { _INTERNAL_flushLogsBuffer } from './logs/internal'; +import { _INTERNAL_flushMetricsBuffer } from './metrics/internal'; import type { Scope } from './scope'; import { updateSession } from './session'; -import { - getDynamicSamplingContextFromScope, - getDynamicSamplingContextFromSpan, -} from './tracing/dynamicSamplingContext'; +import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; import type { Breadcrumb, BreadcrumbHint, FetchBreadcrumbHint, XhrBreadcrumbHint } from './types-hoist/breadcrumb'; import type { CheckIn, MonitorConfig } from './types-hoist/checkin'; import type { EventDropReason, Outcome } from './types-hoist/clientreport'; -import type { TraceContext } from './types-hoist/context'; import type { DataCategory } from './types-hoist/datacategory'; import type { DsnComponents } from './types-hoist/dsn'; import type { DynamicSamplingContext, Envelope } from './types-hoist/envelope'; @@ -25,6 +23,7 @@ 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 { Primitive } from './types-hoist/misc'; import type { ClientOptions } from './types-hoist/options'; import type { ParameterizedString } from './types-hoist/parameterize'; import type { RequestEventData } from './types-hoist/request'; @@ -45,7 +44,7 @@ import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; -import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; +import { showSpanDropWarning } from './utils/spanUtils'; import { rejectedSyncPromise } from './utils/syncpromise'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; @@ -55,6 +54,9 @@ const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing const INTERNAL_ERROR_SYMBOL = Symbol.for('SentryInternalError'); const DO_NOT_SEND_EVENT_SYMBOL = Symbol.for('SentryDoNotSendEventError'); +// Default interval for flushing logs and metrics (5 seconds) +const DEFAULT_FLUSH_INTERVAL = 5000; + interface InternalError { message: string; [INTERNAL_ERROR_SYMBOL]: true; @@ -87,6 +89,57 @@ function _isDoNotSendEventError(error: unknown): error is DoNotSendEventError { return !!error && typeof error === 'object' && DO_NOT_SEND_EVENT_SYMBOL in error; } +/** + * Sets up weight-based flushing for logs or metrics. + * This helper function encapsulates the common pattern of: + * 1. Tracking accumulated weight of items + * 2. Flushing when weight exceeds threshold (800KB) + * 3. Flushing after idle timeout if no new items arrive + * + * Uses closure variables to track weight and timeout state. + */ +function setupWeightBasedFlushing< + T, + AfterCaptureHook extends 'afterCaptureLog' | 'afterCaptureMetric', + FlushHook extends 'flushLogs' | 'flushMetrics', +>( + client: Client, + afterCaptureHook: AfterCaptureHook, + flushHook: FlushHook, + estimateSizeFn: (item: T) => number, + flushFn: (client: Client) => void, +): void { + // Track weight and timeout in closure variables + let weight = 0; + let flushTimeout: ReturnType | undefined; + + // @ts-expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe + client.on(flushHook, () => { + weight = 0; + clearTimeout(flushTimeout); + }); + + // @ts-expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe + client.on(afterCaptureHook, (item: T) => { + weight += estimateSizeFn(item); + + // We flush the buffer if it exceeds 0.8 MB + // The weight is a rough estimate, so we flush way before the payload gets too big. + if (weight >= 800_000) { + flushFn(client); + } else { + clearTimeout(flushTimeout); + flushTimeout = setTimeout(() => { + flushFn(client); + }, DEFAULT_FLUSH_INTERVAL); + } + }); + + client.on('flush', () => { + flushFn(client); + }); +} + /** * Base implementation for all JavaScript SDK clients. * @@ -173,6 +226,22 @@ export abstract class Client { url, }); } + + // Setup log flushing with weight and timeout tracking + if (this._options.enableLogs) { + setupWeightBasedFlushing(this, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer); + } + + // Setup metric flushing with weight and timeout tracking + if (this._options._experiments?.enableMetrics) { + setupWeightBasedFlushing( + this, + 'afterCaptureMetric', + 'flushMetrics', + estimateMetricSizeInBytes, + _INTERNAL_flushMetricsBuffer, + ); + } } /** @@ -1438,21 +1507,82 @@ function isTransactionEvent(event: Event): event is TransactionEvent { return event.type === 'transaction'; } -/** Extract trace information from scope */ -export function _getTraceInfoFromScope( - client: Client, - scope: Scope | undefined, -): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { - if (!scope) { - return [undefined, undefined]; +/** + * Estimate the size of a metric in bytes. + * + * @param metric - The metric to estimate the size of. + * @returns The estimated size of the metric in bytes. + */ +function estimateMetricSizeInBytes(metric: Metric): number { + let weight = 0; + + // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. + if (metric.name) { + weight += metric.name.length * 2; } - return withScope(scope, () => { - const span = getActiveSpan(); - const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); - const dynamicSamplingContext = span - ? getDynamicSamplingContextFromSpan(span) - : getDynamicSamplingContextFromScope(client, scope); - return [dynamicSamplingContext, traceContext]; + // Add weight for the value + if (typeof metric.value === 'string') { + weight += metric.value.length * 2; + } else { + weight += 8; // number + } + + return weight + estimateAttributesSizeInBytes(metric.attributes); +} + +/** + * Estimate the size of a log in bytes. + * + * @param log - The log to estimate the size of. + * @returns The estimated size of the log in bytes. + */ +function estimateLogSizeInBytes(log: Log): number { + let weight = 0; + + // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. + if (log.message) { + weight += log.message.length * 2; + } + + return weight + estimateAttributesSizeInBytes(log.attributes); +} + +/** + * Estimate the size of attributes in bytes. + * + * @param attributes - The attributes object to estimate the size of. + * @returns The estimated size of the attributes in bytes. + */ +function estimateAttributesSizeInBytes(attributes: Record | undefined): number { + if (!attributes) { + return 0; + } + + let weight = 0; + + Object.values(attributes).forEach(value => { + if (Array.isArray(value)) { + weight += value.length * estimatePrimitiveSizeInBytes(value[0]); + } else if (isPrimitive(value)) { + weight += estimatePrimitiveSizeInBytes(value); + } else { + // For objects values, we estimate the size of the object as 100 bytes + weight += 100; + } }); + + return weight; +} + +function estimatePrimitiveSizeInBytes(value: Primitive): number { + if (typeof value === 'string') { + return value.length * 2; + } else if (typeof value === 'number') { + return 8; + } else if (typeof value === 'boolean') { + return 4; + } + + return 0; } diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index b3bda05d97f7..601d9be29cb6 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -1,6 +1,5 @@ import { getGlobalSingleton } from '../carrier'; import type { Client } from '../client'; -import { _getTraceInfoFromScope } from '../client'; import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope, ScopeData } from '../scope'; @@ -11,6 +10,7 @@ import { consoleSandbox, debug } from '../utils/debug-logger'; import { isParameterizedString } from '../utils/is'; import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; +import { _getTraceInfoFromScope } from '../utils/trace-info'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; import { createLogEnvelope } from './envelope'; diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 0f16d98b790e..f16352523700 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -1,15 +1,15 @@ import { getGlobalSingleton } from '../carrier'; import type { Client } from '../client'; -import { _getTraceInfoFromScope } from '../client'; import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope, ScopeData } from '../scope'; import type { Integration } from '../types-hoist/integration'; import type { Metric, SerializedMetric, SerializedMetricAttributeValue } from '../types-hoist/metric'; import { mergeScopeData } from '../utils/applyScopeDataToEvent'; -import { consoleSandbox, debug } from '../utils/debug-logger'; +import { debug } from '../utils/debug-logger'; import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; +import { _getTraceInfoFromScope } from '../utils/trace-info'; import { createMetricEnvelope } from './envelope'; const MAX_METRIC_BUFFER_SIZE = 100; @@ -210,10 +210,7 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal attributes: serializedAttributes, }; - consoleSandbox(() => { - // eslint-disable-next-line no-console - DEBUG_BUILD && console.log('[Metric]', serializedMetric); - }); + DEBUG_BUILD && debug.log('[Metric]', serializedMetric); captureSerializedMetric(client, serializedMetric); diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 761d4aca7cd7..9d037eb3b7c3 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -1,28 +1,20 @@ import { createCheckInEnvelope } from './checkin'; -import { _getTraceInfoFromScope, Client } from './client'; +import { Client } from './client'; import { getIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; -import { _INTERNAL_flushLogsBuffer } from './logs/internal'; -import { _INTERNAL_flushMetricsBuffer } from './metrics/internal'; import type { Scope } from './scope'; import { registerSpanErrorInstrumentation } from './tracing'; import type { CheckIn, MonitorConfig, SerializedCheckIn } from './types-hoist/checkin'; import type { Event, EventHint } from './types-hoist/event'; -import type { Log } from './types-hoist/log'; -import type { Metric } from './types-hoist/metric'; -import type { Primitive } from './types-hoist/misc'; import type { ClientOptions } from './types-hoist/options'; import type { ParameterizedString } from './types-hoist/parameterize'; import type { SeverityLevel } from './types-hoist/severity'; import type { BaseTransportOptions } from './types-hoist/transport'; import { debug } from './utils/debug-logger'; import { eventFromMessage, eventFromUnknownInput } from './utils/eventbuilder'; -import { isPrimitive } from './utils/is'; import { uuid4 } from './utils/misc'; import { resolvedSyncPromise } from './utils/syncpromise'; - -// TODO: Make this configurable -const DEFAULT_LOG_FLUSH_INTERVAL = 5000; +import { _getTraceInfoFromScope } from './utils/trace-info'; export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; @@ -36,11 +28,6 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends Client { - private _logFlushIdleTimeout: ReturnType | undefined; - private _logWeight: number; - private _metricFlushIdleTimeout: ReturnType | undefined; - private _metricWeight: number; - /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. @@ -50,69 +37,6 @@ export class ServerRuntimeClient< registerSpanErrorInstrumentation(); super(options); - - this._logWeight = 0; - this._metricWeight = 0; - - if (this._options.enableLogs) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const client = this; - - client.on('flushLogs', () => { - client._logWeight = 0; - clearTimeout(client._logFlushIdleTimeout); - }); - - client.on('afterCaptureLog', log => { - client._logWeight += estimateLogSizeInBytes(log); - - // We flush the logs buffer if it exceeds 0.8 MB - // The log weight is a rough estimate, so we flush way before - // the payload gets too big. - if (client._logWeight >= 800_000) { - _INTERNAL_flushLogsBuffer(client); - } else { - // start an idle timeout to flush the logs buffer if no logs are captured for a while - client._logFlushIdleTimeout = setTimeout(() => { - _INTERNAL_flushLogsBuffer(client); - }, DEFAULT_LOG_FLUSH_INTERVAL); - } - }); - - client.on('flush', () => { - _INTERNAL_flushLogsBuffer(client); - }); - } - - if (this._options._experiments?.enableMetrics) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const client = this; - - client.on('flushMetrics', () => { - client._metricWeight = 0; - clearTimeout(client._metricFlushIdleTimeout); - }); - - client.on('afterCaptureMetric', metric => { - client._metricWeight += estimateMetricSizeInBytes(metric); - - // We flush the metrics buffer if it exceeds 0.8 MB - // The metric weight is a rough estimate, so we flush way before - // the payload gets too big. - if (client._metricWeight >= 800_000) { - _INTERNAL_flushMetricsBuffer(client); - } else { - // start an idle timeout to flush the metrics buffer if no metrics are captured for a while - client._metricFlushIdleTimeout = setTimeout(() => { - _INTERNAL_flushMetricsBuffer(client); - }, DEFAULT_LOG_FLUSH_INTERVAL); - } - }); - - client.on('flush', () => { - _INTERNAL_flushMetricsBuffer(client); - }); - } } /** @@ -267,82 +191,3 @@ function setCurrentRequestSessionErroredOrCrashed(eventHint?: EventHint): void { } } } - -/** - * Estimate the size of a metric in bytes. - * - * @param metric - The metric to estimate the size of. - * @returns The estimated size of the metric in bytes. - */ -function estimateMetricSizeInBytes(metric: Metric): number { - let weight = 0; - - // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. - if (metric.name) { - weight += metric.name.length * 2; - } - - // Add weight for the value - if (typeof metric.value === 'string') { - weight += metric.value.length * 2; - } else { - weight += 8; // number - } - - if (metric.attributes) { - Object.values(metric.attributes).forEach(value => { - if (Array.isArray(value)) { - weight += value.length * estimatePrimitiveSizeInBytes(value[0]); - } else if (isPrimitive(value)) { - weight += estimatePrimitiveSizeInBytes(value); - } else { - // For objects values, we estimate the size of the object as 100 bytes - weight += 100; - } - }); - } - - return weight; -} - -/** - * Estimate the size of a log in bytes. - * - * @param log - The log to estimate the size of. - * @returns The estimated size of the log in bytes. - */ -function estimateLogSizeInBytes(log: Log): number { - let weight = 0; - - // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. - if (log.message) { - weight += log.message.length * 2; - } - - if (log.attributes) { - Object.values(log.attributes).forEach(value => { - if (Array.isArray(value)) { - weight += value.length * estimatePrimitiveSizeInBytes(value[0]); - } else if (isPrimitive(value)) { - weight += estimatePrimitiveSizeInBytes(value); - } else { - // For objects values, we estimate the size of the object as 100 bytes - weight += 100; - } - }); - } - - return weight; -} - -function estimatePrimitiveSizeInBytes(value: Primitive): number { - if (typeof value === 'string') { - return value.length * 2; - } else if (typeof value === 'number') { - return 8; - } else if (typeof value === 'boolean') { - return 4; - } - - return 0; -} diff --git a/packages/core/src/utils/trace-info.ts b/packages/core/src/utils/trace-info.ts new file mode 100644 index 000000000000..d7d0be69ca07 --- /dev/null +++ b/packages/core/src/utils/trace-info.ts @@ -0,0 +1,29 @@ +import type { Client } from '../client'; +import { getTraceContextFromScope, withScope } from '../currentScopes'; +import type { Scope } from '../scope'; +import { + getDynamicSamplingContextFromScope, + getDynamicSamplingContextFromSpan, +} from '../tracing/dynamicSamplingContext'; +import type { TraceContext } from '../types-hoist/context'; +import type { DynamicSamplingContext } from '../types-hoist/envelope'; +import { getActiveSpan, spanToTraceContext } from './spanUtils'; + +/** Extract trace information from scope */ +export function _getTraceInfoFromScope( + client: Client, + scope: Scope | undefined, +): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { + if (!scope) { + return [undefined, undefined]; + } + + return withScope(scope, () => { + const span = getActiveSpan(); + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(client, scope); + return [dynamicSamplingContext, traceContext]; + }); +} diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index c7cbe7ab4a97..ae324aa40f9f 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -12,6 +12,8 @@ import { withMonitor, } from '../../src'; import * as integrationModule from '../../src/integration'; +import { _INTERNAL_captureLog } from '../../src/logs/internal'; +import { _INTERNAL_captureMetric } from '../../src/metrics/internal'; import type { Envelope } from '../../src/types-hoist/envelope'; import type { ErrorEvent, Event, TransactionEvent } from '../../src/types-hoist/event'; import type { SpanJSON } from '../../src/types-hoist/span'; @@ -2599,4 +2601,209 @@ describe('Client', () => { await expect(promise).rejects.toThrowError(error); }); }); + + describe('log weight-based flushing', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('flushes logs when weight exceeds 800KB', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a large log message that will exceed the 800KB threshold + const largeMessage = 'x'.repeat(400_000); // 400KB string + _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + }); + + it('accumulates log weight without flushing when under threshold', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a log message that won't exceed the threshold + const message = 'x'.repeat(100_000); // 100KB string + _INTERNAL_captureLog({ message, level: 'info' }, scope); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + }); + + it('flushes logs after idle timeout', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add a log which will trigger afterCaptureLog event + _INTERNAL_captureLog({ message: 'test log', level: 'info' }, scope); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + + // Fast forward the idle timeout (5 seconds) + vi.advanceTimersByTime(5000); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + }); + + it('resets idle timeout when new logs are captured', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add initial log + _INTERNAL_captureLog({ message: 'test log 1', level: 'info' }, scope); + + // Fast forward part of the idle timeout + vi.advanceTimersByTime(2500); + + // Add another log which should reset the timeout + _INTERNAL_captureLog({ message: 'test log 2', level: 'info' }, scope); + + // Fast forward the remaining time + vi.advanceTimersByTime(2500); + + // Should not have flushed yet since timeout was reset + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + + // Fast forward the full timeout + vi.advanceTimersByTime(5000); + + // Now should have flushed both logs + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + }); + + it('flushes logs on flush event', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add some logs + _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); + _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); + + // Trigger flush event + client.emit('flush'); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + }); + + it('does not flush logs when logs are disabled', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create a large log message + const largeMessage = 'x'.repeat(400_000); + _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + }); + }); + + describe('metric weight-based flushing', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('flushes metrics when weight exceeds 800KB', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableMetrics: true }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create large metrics that will exceed the 800KB threshold + const largeValue = 'x'.repeat(400_000); // 400KB string + _INTERNAL_captureMetric({ name: 'large_metric', value: largeValue, type: 'counter', attributes: {} }, { scope }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + }); + + it('accumulates metric weight without flushing when under threshold', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableMetrics: true }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Create metrics that won't exceed the threshold + _INTERNAL_captureMetric({ name: 'test_metric', value: 42, type: 'counter', attributes: {} }, { scope }); + + expect(sendEnvelopeSpy).not.toHaveBeenCalled(); + }); + + it('flushes metrics on flush event', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableMetrics: true }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + // Add some metrics + _INTERNAL_captureMetric({ name: 'metric1', value: 1, type: 'counter', attributes: {} }, { scope }); + _INTERNAL_captureMetric({ name: 'metric2', value: 2, type: 'counter', attributes: {} }, { scope }); + + // Trigger flush event + client.emit('flush'); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/core/test/lib/integrations/consola.test.ts b/packages/core/test/lib/integrations/consola.test.ts index 186e5fdc295e..a5c68184e03b 100644 --- a/packages/core/test/lib/integrations/consola.test.ts +++ b/packages/core/test/lib/integrations/consola.test.ts @@ -8,6 +8,7 @@ import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; // Mock dependencies vi.mock('../../../src/logs/internal', () => ({ _INTERNAL_captureLog: vi.fn(), + _INTERNAL_flushLogsBuffer: vi.fn(), })); vi.mock('../../../src/logs/utils', async actual => ({ diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index 9fcb431af864..525ee514c1a2 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, test, vi } from 'vitest'; import { createTransport, Scope } from '../../src'; -import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '../../src/logs/internal'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; import type { Event, EventHint } from '../../src/types-hoist/event'; @@ -206,106 +205,4 @@ describe('ServerRuntimeClient', () => { ]); }); }); - - describe('log weight-based flushing', () => { - it('flushes logs when weight exceeds 800KB', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Create a large log message that will exceed the 800KB threshold - const largeMessage = 'x'.repeat(400_000); // 400KB string - _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(client['_logWeight']).toBe(0); // Weight should be reset after flush - }); - - it('accumulates log weight without flushing when under threshold', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Create a log message that won't exceed the threshold - const message = 'x'.repeat(100_000); // 100KB string - _INTERNAL_captureLog({ message, level: 'info' }, scope); - - expect(sendEnvelopeSpy).not.toHaveBeenCalled(); - expect(client['_logWeight']).toBeGreaterThan(0); - }); - - it('flushes logs on flush event', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Add some logs - _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); - _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); - - // Trigger flush directly - _INTERNAL_flushLogsBuffer(client); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(client['_logWeight']).toBe(0); // Weight should be reset after flush - }); - - it('does not flush logs when logs are disabled', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Create a large log message - const largeMessage = 'x'.repeat(400_000); - _INTERNAL_captureLog({ message: largeMessage, level: 'info' }, scope); - - expect(sendEnvelopeSpy).not.toHaveBeenCalled(); - expect(client['_logWeight']).toBe(0); - }); - - it('flushes logs when flush event is triggered', () => { - const options = getDefaultClientOptions({ - dsn: PUBLIC_DSN, - enableLogs: true, - }); - client = new ServerRuntimeClient(options); - const scope = new Scope(); - scope.setClient(client); - - const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); - - // Add some logs - _INTERNAL_captureLog({ message: 'test1', level: 'info' }, scope); - _INTERNAL_captureLog({ message: 'test2', level: 'info' }, scope); - - // Trigger flush event - client.emit('flush'); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(client['_logWeight']).toBe(0); // Weight should be reset after flush - }); - }); });