11/* eslint-disable max-lines */
22import { getEnvelopeEndpointWithUrlEncodedAuth } from './api' ;
33import { DEFAULT_ENVIRONMENT } from './constants' ;
4- import { getCurrentScope , getIsolationScope , getTraceContextFromScope , withScope } from './currentScopes' ;
4+ import { getCurrentScope , getIsolationScope , getTraceContextFromScope } from './currentScopes' ;
55import { DEBUG_BUILD } from './debug-build' ;
66import { createEventEnvelope , createSessionEnvelope } from './envelope' ;
77import type { IntegrationIndex } from './integration' ;
88import { afterSetupIntegrations , setupIntegration , setupIntegrations } from './integration' ;
9+ import { _INTERNAL_flushLogsBuffer } from './logs/internal' ;
10+ import { _INTERNAL_flushMetricsBuffer } from './metrics/internal' ;
911import type { Scope } from './scope' ;
1012import { updateSession } from './session' ;
11- import {
12- getDynamicSamplingContextFromScope ,
13- getDynamicSamplingContextFromSpan ,
14- } from './tracing/dynamicSamplingContext' ;
13+ import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext' ;
1514import type { Breadcrumb , BreadcrumbHint , FetchBreadcrumbHint , XhrBreadcrumbHint } from './types-hoist/breadcrumb' ;
1615import type { CheckIn , MonitorConfig } from './types-hoist/checkin' ;
1716import type { EventDropReason , Outcome } from './types-hoist/clientreport' ;
18- import type { TraceContext } from './types-hoist/context' ;
1917import type { DataCategory } from './types-hoist/datacategory' ;
2018import type { DsnComponents } from './types-hoist/dsn' ;
2119import type { DynamicSamplingContext , Envelope } from './types-hoist/envelope' ;
@@ -25,6 +23,7 @@ import type { FeedbackEvent } from './types-hoist/feedback';
2523import type { Integration } from './types-hoist/integration' ;
2624import type { Log } from './types-hoist/log' ;
2725import type { Metric } from './types-hoist/metric' ;
26+ import type { Primitive } from './types-hoist/misc' ;
2827import type { ClientOptions } from './types-hoist/options' ;
2928import type { ParameterizedString } from './types-hoist/parameterize' ;
3029import type { RequestEventData } from './types-hoist/request' ;
@@ -45,7 +44,7 @@ import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc';
4544import { parseSampleRate } from './utils/parseSampleRate' ;
4645import { prepareEvent } from './utils/prepareEvent' ;
4746import { reparentChildSpans , shouldIgnoreSpan } from './utils/should-ignore-span' ;
48- import { getActiveSpan , showSpanDropWarning , spanToTraceContext } from './utils/spanUtils' ;
47+ import { showSpanDropWarning } from './utils/spanUtils' ;
4948import { rejectedSyncPromise } from './utils/syncpromise' ;
5049import { convertSpanJsonToTransactionEvent , convertTransactionEventToSpanJson } from './utils/transactionEvent' ;
5150
@@ -55,6 +54,9 @@ const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing
5554const INTERNAL_ERROR_SYMBOL = Symbol . for ( 'SentryInternalError' ) ;
5655const DO_NOT_SEND_EVENT_SYMBOL = Symbol . for ( 'SentryDoNotSendEventError' ) ;
5756
57+ // Default interval for flushing logs and metrics (5 seconds)
58+ const DEFAULT_FLUSH_INTERVAL = 5000 ;
59+
5860interface InternalError {
5961 message : string ;
6062 [ INTERNAL_ERROR_SYMBOL ] : true ;
@@ -87,6 +89,57 @@ function _isDoNotSendEventError(error: unknown): error is DoNotSendEventError {
8789 return ! ! error && typeof error === 'object' && DO_NOT_SEND_EVENT_SYMBOL in error ;
8890}
8991
92+ /**
93+ * Sets up weight-based flushing for logs or metrics.
94+ * This helper function encapsulates the common pattern of:
95+ * 1. Tracking accumulated weight of items
96+ * 2. Flushing when weight exceeds threshold (800KB)
97+ * 3. Flushing after idle timeout if no new items arrive
98+ *
99+ * Uses closure variables to track weight and timeout state.
100+ */
101+ function setupWeightBasedFlushing <
102+ T ,
103+ AfterCaptureHook extends 'afterCaptureLog' | 'afterCaptureMetric' ,
104+ FlushHook extends 'flushLogs' | 'flushMetrics' ,
105+ > (
106+ client : Client ,
107+ afterCaptureHook : AfterCaptureHook ,
108+ flushHook : FlushHook ,
109+ estimateSizeFn : ( item : T ) => number ,
110+ flushFn : ( client : Client ) => void ,
111+ ) : void {
112+ // Track weight and timeout in closure variables
113+ let weight = 0 ;
114+ let flushTimeout : ReturnType < typeof setTimeout > | undefined ;
115+
116+ // @ts -expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe
117+ client . on ( flushHook , ( ) => {
118+ weight = 0 ;
119+ clearTimeout ( flushTimeout ) ;
120+ } ) ;
121+
122+ // @ts -expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe
123+ client . on ( afterCaptureHook , ( item : T ) => {
124+ weight += estimateSizeFn ( item ) ;
125+
126+ // We flush the buffer if it exceeds 0.8 MB
127+ // The weight is a rough estimate, so we flush way before the payload gets too big.
128+ if ( weight >= 800_000 ) {
129+ flushFn ( client ) ;
130+ } else {
131+ clearTimeout ( flushTimeout ) ;
132+ flushTimeout = setTimeout ( ( ) => {
133+ flushFn ( client ) ;
134+ } , DEFAULT_FLUSH_INTERVAL ) ;
135+ }
136+ } ) ;
137+
138+ client . on ( 'flush' , ( ) => {
139+ flushFn ( client ) ;
140+ } ) ;
141+ }
142+
90143/**
91144 * Base implementation for all JavaScript SDK clients.
92145 *
@@ -173,6 +226,22 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
173226 url,
174227 } ) ;
175228 }
229+
230+ // Setup log flushing with weight and timeout tracking
231+ if ( this . _options . enableLogs ) {
232+ setupWeightBasedFlushing ( this , 'afterCaptureLog' , 'flushLogs' , estimateLogSizeInBytes , _INTERNAL_flushLogsBuffer ) ;
233+ }
234+
235+ // Setup metric flushing with weight and timeout tracking
236+ if ( this . _options . _experiments ?. enableMetrics ) {
237+ setupWeightBasedFlushing (
238+ this ,
239+ 'afterCaptureMetric' ,
240+ 'flushMetrics' ,
241+ estimateMetricSizeInBytes ,
242+ _INTERNAL_flushMetricsBuffer ,
243+ ) ;
244+ }
176245 }
177246
178247 /**
@@ -1438,21 +1507,82 @@ function isTransactionEvent(event: Event): event is TransactionEvent {
14381507 return event . type === 'transaction' ;
14391508}
14401509
1441- /** Extract trace information from scope */
1442- export function _getTraceInfoFromScope (
1443- client : Client ,
1444- scope : Scope | undefined ,
1445- ) : [ dynamicSamplingContext : Partial < DynamicSamplingContext > | undefined , traceContext : TraceContext | undefined ] {
1446- if ( ! scope ) {
1447- return [ undefined , undefined ] ;
1510+ /**
1511+ * Estimate the size of a metric in bytes.
1512+ *
1513+ * @param metric - The metric to estimate the size of.
1514+ * @returns The estimated size of the metric in bytes.
1515+ */
1516+ function estimateMetricSizeInBytes ( metric : Metric ) : number {
1517+ let weight = 0 ;
1518+
1519+ // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16.
1520+ if ( metric . name ) {
1521+ weight += metric . name . length * 2 ;
14481522 }
14491523
1450- return withScope ( scope , ( ) => {
1451- const span = getActiveSpan ( ) ;
1452- const traceContext = span ? spanToTraceContext ( span ) : getTraceContextFromScope ( scope ) ;
1453- const dynamicSamplingContext = span
1454- ? getDynamicSamplingContextFromSpan ( span )
1455- : getDynamicSamplingContextFromScope ( client , scope ) ;
1456- return [ dynamicSamplingContext , traceContext ] ;
1524+ // Add weight for the value
1525+ if ( typeof metric . value === 'string' ) {
1526+ weight += metric . value . length * 2 ;
1527+ } else {
1528+ weight += 8 ; // number
1529+ }
1530+
1531+ return weight + estimateAttributesSizeInBytes ( metric . attributes ) ;
1532+ }
1533+
1534+ /**
1535+ * Estimate the size of a log in bytes.
1536+ *
1537+ * @param log - The log to estimate the size of.
1538+ * @returns The estimated size of the log in bytes.
1539+ */
1540+ function estimateLogSizeInBytes ( log : Log ) : number {
1541+ let weight = 0 ;
1542+
1543+ // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16.
1544+ if ( log . message ) {
1545+ weight += log . message . length * 2 ;
1546+ }
1547+
1548+ return weight + estimateAttributesSizeInBytes ( log . attributes ) ;
1549+ }
1550+
1551+ /**
1552+ * Estimate the size of attributes in bytes.
1553+ *
1554+ * @param attributes - The attributes object to estimate the size of.
1555+ * @returns The estimated size of the attributes in bytes.
1556+ */
1557+ function estimateAttributesSizeInBytes ( attributes : Record < string , unknown > | undefined ) : number {
1558+ if ( ! attributes ) {
1559+ return 0 ;
1560+ }
1561+
1562+ let weight = 0 ;
1563+
1564+ Object . values ( attributes ) . forEach ( value => {
1565+ if ( Array . isArray ( value ) ) {
1566+ weight += value . length * estimatePrimitiveSizeInBytes ( value [ 0 ] ) ;
1567+ } else if ( isPrimitive ( value ) ) {
1568+ weight += estimatePrimitiveSizeInBytes ( value ) ;
1569+ } else {
1570+ // For objects values, we estimate the size of the object as 100 bytes
1571+ weight += 100 ;
1572+ }
14571573 } ) ;
1574+
1575+ return weight ;
1576+ }
1577+
1578+ function estimatePrimitiveSizeInBytes ( value : Primitive ) : number {
1579+ if ( typeof value === 'string' ) {
1580+ return value . length * 2 ;
1581+ } else if ( typeof value === 'number' ) {
1582+ return 8 ;
1583+ } else if ( typeof value === 'boolean' ) {
1584+ return 4 ;
1585+ }
1586+
1587+ return 0 ;
14581588}
0 commit comments