Skip to content
Merged
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
159 changes: 0 additions & 159 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ 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 } from './tracing/dynamicSamplingContext';
Expand All @@ -26,7 +24,6 @@ import type { FeedbackEvent } from './types/feedback';
import type { Integration } from './types/integration';
import type { Log } from './types/log';
import type { Metric } from './types/metric';
import type { Primitive } from './types/misc';
import type { ClientOptions } from './types/options';
import type { ParameterizedString } from './types/parameterize';
import type { ReplayEndEvent, ReplayStartEvent } from './types/replay';
Expand All @@ -53,7 +50,6 @@ import { safeMathRandom } from './utils/randomSafeContext';
import { safeCallback } from './utils/safeCallback';
import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span';
import { rejectedSyncPromise } from './utils/syncpromise';
import { safeUnref } from './utils/timer';
import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';
import { maybeWarnAboutIgnoredTransactionOptions } from './utils/warnAboutIgnoredTransactionOptions';
import { resolveDataCollectionOptions } from './utils/data-collection/resolveDataCollectionOptions';
Expand All @@ -65,9 +61,6 @@ 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;

// Clones of a scope keep their own reference to its client, so unbinding a
// closed client from one scope does not reach the others. The mark lives on
// the client under a `Symbol.for` key, so every copy of the SDK can read it.
Expand Down Expand Up @@ -120,70 +113,6 @@ function _isDoNotSendEventError(error: unknown): error is DoNotSendEventError {
return isObjectLike(error) && 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 timeout period from the first item
*
* 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<typeof setTimeout> | undefined;
let isTimerActive = false;

// @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);
isTimerActive = false;
});

// @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 if (!isTimerActive) {
const flushInterval = client.getOptions()._flushInterval ?? DEFAULT_FLUSH_INTERVAL;
if (flushInterval > 0) {
// Only start timer if one isn't already running.
// This prevents flushing being delayed by items that arrive close to the timeout limit
// and thus resetting the flushing timeout and delaying items being flushed.
isTimerActive = true;
// Use safeUnref so the timer doesn't prevent the process from exiting
flushTimeout = safeUnref(
setTimeout(() => {
flushFn(client);
// Note: isTimerActive is reset by the flushHook handler above, not here,
// to avoid race conditions when new items arrive during the flush.
}, flushInterval),
);
}
}
});

client.on('flush', () => {
flushFn(client);
});
}

/**
* Base implementation for all JavaScript SDK clients.
*
Expand Down Expand Up @@ -313,18 +242,6 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
url,
});
}

// Setup log flushing with weight and timeout tracking
setupWeightBasedFlushing(this, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer);

// Setup metric flushing with weight and timeout tracking
setupWeightBasedFlushing(
this,
'afterCaptureMetric',
'flushMetrics',
estimateMetricSizeInBytes,
_INTERNAL_flushMetricsBuffer,
);
}

/**
Expand Down Expand Up @@ -1874,79 +1791,3 @@ function isErrorEvent(event: Event): event is ErrorEvent {
function isTransactionEvent(event: Event): event is TransactionEvent {
return event.type === 'transaction';
}

/**
* 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 number
weight += 8;

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<string, unknown> | 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;
}
3 changes: 3 additions & 0 deletions packages/core/src/logs/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { getActiveSpan } from '../utils/spanUtils';
import { timestampInSeconds } from '../utils/time';
import { getSequenceAttribute } from '../utils/timestampSequence';
import { _getTraceInfoFromScope } from '../utils/trace-info';
import { estimateLogSizeInBytes, setupWeightBasedFlushing } from '../utils/weightBasedFlushing';
import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants';
import { createLogEnvelope } from './envelope';

Expand Down Expand Up @@ -64,6 +65,8 @@ export function _INTERNAL_captureSerializedLog(client: Client, serializedLog: Se
const logBuffer = _INTERNAL_getLogBuffer(client);

if (logBuffer === undefined) {
// Set up flushing on the first log, so apps that never capture logs don't bundle the flush code.
setupWeightBasedFlushing(client, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer);
bufferMap.set(client, [serializedLog]);
} else {
if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) {
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/metrics/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { getActiveSpan } from '../utils/spanUtils';
import { timestampInSeconds } from '../utils/time';
import { getSequenceAttribute } from '../utils/timestampSequence';
import { _getTraceInfoFromScope } from '../utils/trace-info';
import { estimateMetricSizeInBytes, setupWeightBasedFlushing } from '../utils/weightBasedFlushing';
import { createMetricEnvelope } from './envelope';

const MAX_METRIC_BUFFER_SIZE = 1000;
Expand Down Expand Up @@ -61,6 +62,14 @@ export function _INTERNAL_captureSerializedMetric(client: Client, serializedMetr
const metricBuffer = _INTERNAL_getMetricBuffer(client);

if (metricBuffer === undefined) {
// Set up flushing on the first metric, so apps that never capture metrics don't bundle the flush code.
setupWeightBasedFlushing(
client,
'afterCaptureMetric',
'flushMetrics',
estimateMetricSizeInBytes,
_INTERNAL_flushMetricsBuffer,
);
bufferMap.set(client, [serializedMetric]);
} else {
if (metricBuffer.length >= MAX_METRIC_BUFFER_SIZE) {
Expand Down
149 changes: 149 additions & 0 deletions packages/core/src/utils/weightBasedFlushing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import type { Client } from '../client';
import type { Log } from '../types/log';
import type { Metric } from '../types/metric';
import type { Primitive } from '../types/misc';
import { isPrimitive } from './is';
import { safeUnref } from './timer';

// Default interval for flushing logs and metrics (5 seconds)
const DEFAULT_FLUSH_INTERVAL = 5000;

/**
* 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 timeout period from the first item
*
* Uses closure variables to track weight and timeout state.
*/
export 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<typeof setTimeout> | undefined;
let isTimerActive = false;

// @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);
isTimerActive = false;
});

// @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 if (!isTimerActive) {
const flushInterval = client.getOptions()._flushInterval ?? DEFAULT_FLUSH_INTERVAL;
if (flushInterval > 0) {
// Only start timer if one isn't already running.
// This prevents flushing being delayed by items that arrive close to the timeout limit
// and thus resetting the flushing timeout and delaying items being flushed.
isTimerActive = true;
// Use safeUnref so the timer doesn't prevent the process from exiting
flushTimeout = safeUnref(
setTimeout(() => {
flushFn(client);
// Note: isTimerActive is reset by the flushHook handler above, not here,
// to avoid race conditions when new items arrive during the flush.
}, flushInterval),
);
}
}
});

client.on('flush', () => {
flushFn(client);
});
}

/**
* 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.
*/
export 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 number
weight += 8;

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.
*/
export 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<string, unknown> | 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;
}
Loading
Loading