diff --git a/.size-limit.js b/.size-limit.js index f4bf45b47b40..0b9cd2b00cb2 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -124,7 +124,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'logger'), gzip: true, - limit: '27 KB', + limit: '28 KB', }, { name: '@sentry/browser (incl. Metrics & Logs)', @@ -255,21 +255,21 @@ module.exports = [ path: createCDNPath('bundle.tracing.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '131 KB', + limit: '132 KB', }, { name: 'CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed', path: createCDNPath('bundle.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '209 KB', + limit: '210 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '245 KB', + limit: '246 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed', diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts index 40c2d18d29bd..7315e8cf4f36 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts @@ -34,6 +34,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'console.trace {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 123, type: 'integer' }, 'sentry.message.parameter.1': { value: false, type: 'boolean' }, @@ -49,6 +50,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'console.debug {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 123, type: 'integer' }, 'sentry.message.parameter.1': { value: false, type: 'boolean' }, @@ -64,6 +66,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'console.log {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 123, type: 'integer' }, 'sentry.message.parameter.1': { value: false, type: 'boolean' }, @@ -79,6 +82,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'console.info {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 123, type: 'integer' }, 'sentry.message.parameter.1': { value: false, type: 'boolean' }, @@ -94,6 +98,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'console.warn {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 123, type: 'integer' }, 'sentry.message.parameter.1': { value: false, type: 'boolean' }, @@ -109,6 +114,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'console.error {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 123, type: 'integer' }, 'sentry.message.parameter.1': { value: false, type: 'boolean' }, @@ -124,6 +130,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -136,6 +143,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'Object: {}', type: 'string' }, 'sentry.message.parameter.0': { value: '{"key":"value","nested":{"prop":123}}', type: 'string' }, }, @@ -150,6 +158,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'Array: {}', type: 'string' }, 'sentry.message.parameter.0': { value: '[1,2,3,"string"]', type: 'string' }, }, @@ -164,6 +173,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'Mixed: {} {} {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 'prefix', type: 'string' }, 'sentry.message.parameter.1': { value: '{"obj":true}', type: 'string' }, @@ -181,6 +191,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -193,6 +204,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -205,6 +217,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -217,6 +230,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'first {} {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: 0, type: 'integer' }, 'sentry.message.parameter.1': { value: 1, type: 'integer' }, @@ -233,6 +247,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.log.console', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'hello {} {} {}', type: 'string' }, 'sentry.message.parameter.0': { value: true, type: 'boolean' }, 'sentry.message.parameter.1': { value: 'null', type: 'string' }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts index c02a110046dd..4d7970945436 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/scopeAttributes/test.ts @@ -32,6 +32,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, log_attr: { value: 'log_attr_1', type: 'string' }, }, }, @@ -44,6 +45,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, log_attr: { value: 'log_attr_2', type: 'string' }, }, @@ -57,6 +59,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, log_attr: { value: 'log_attr_3', type: 'string' }, @@ -71,6 +74,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, scope_attr: { value: 200, unit: 'millisecond', type: 'integer' }, @@ -86,6 +90,7 @@ sentryTest('captures logs with scope attributes', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, global_scope_attr: { value: true, type: 'boolean' }, isolation_scope_1_attr: { value: 100, unit: 'millisecond', type: 'integer' }, scope_2_attr: { value: 300, unit: 'millisecond', type: 'integer' }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts index aa2159d13bc1..db6d174820d7 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts @@ -33,6 +33,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -44,6 +45,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -55,6 +57,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -66,6 +69,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -77,6 +81,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -88,6 +93,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -99,6 +105,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'test %s %s %s %s', type: 'string' }, 'sentry.message.parameter.0': { value: 'trace', type: 'string' }, 'sentry.message.parameter.1': { value: 'stringArg', type: 'string' }, @@ -115,6 +122,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'test %s %s %s %s', type: 'string' }, 'sentry.message.parameter.0': { value: 'debug', type: 'string' }, 'sentry.message.parameter.1': { value: 'stringArg', type: 'string' }, @@ -131,6 +139,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'test %s %s %s %s', type: 'string' }, 'sentry.message.parameter.0': { value: 'info', type: 'string' }, 'sentry.message.parameter.1': { value: 'stringArg', type: 'string' }, @@ -147,6 +156,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'test %s %s %s %s', type: 'string' }, 'sentry.message.parameter.0': { value: 'warn', type: 'string' }, 'sentry.message.parameter.1': { value: 'stringArg', type: 'string' }, @@ -163,6 +173,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'test %s %s %s %s', type: 'string' }, 'sentry.message.parameter.0': { value: 'error', type: 'string' }, 'sentry.message.parameter.1': { value: 'stringArg', type: 'string' }, @@ -179,6 +190,7 @@ sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page attributes: { 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, 'sentry.message.template': { value: 'test %s %s %s %s', type: 'string' }, 'sentry.message.parameter.0': { value: 'fatal', type: 'string' }, 'sentry.message.parameter.1': { value: 'stringArg', type: 'string' }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/simple/test.ts b/dev-packages/browser-integration-tests/suites/public-api/metrics/simple/test.ts index f9722fc0bec8..66f44878ac86 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/simple/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/simple/test.ts @@ -40,6 +40,7 @@ sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) 'sentry.environment': { value: 'test', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -55,6 +56,7 @@ sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) 'sentry.environment': { value: 'test', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -70,6 +72,7 @@ sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) 'sentry.environment': { value: 'test', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -85,6 +88,7 @@ sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) 'sentry.environment': { value: 'test', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -102,6 +106,7 @@ sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) 'sentry.environment': { value: 'test', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, { @@ -144,6 +149,10 @@ sentryTest('should capture all metric types', async ({ getLocalTestUrl, page }) type: 'string', value: expect.any(String), }, + 'sentry.timestamp.sequence': { + type: 'integer', + value: expect.any(Number), + }, 'user.email': { type: 'string', value: 'test@example.com', diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts index 5ee5b0954e59..013bf552d772 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts @@ -36,6 +36,10 @@ it('should add server.address attribute to metrics when serverName is set', asyn type: 'string', value: expect.any(String), }, + 'sentry.timestamp.sequence': { + type: 'integer', + value: expect.any(Number), + }, 'server.address': { type: 'string', value: 'mi-servidor.com', diff --git a/dev-packages/node-core-integration-tests/suites/light-mode/logs/test.ts b/dev-packages/node-core-integration-tests/suites/light-mode/logs/test.ts index f1dfde5ecdf8..25096f1be7e5 100644 --- a/dev-packages/node-core-integration-tests/suites/light-mode/logs/test.ts +++ b/dev-packages/node-core-integration-tests/suites/light-mode/logs/test.ts @@ -18,6 +18,7 @@ describe('light mode logs', () => { 'sentry.release': { type: 'string', value: '1.0.0' }, 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node-light' }, 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, 'server.address': { type: 'string', value: expect.any(String) }, }, body: 'test info log', @@ -31,6 +32,7 @@ describe('light mode logs', () => { 'sentry.release': { type: 'string', value: '1.0.0' }, 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node-light' }, 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, 'server.address': { type: 'string', value: expect.any(String) }, }, body: 'test error log', diff --git a/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts index 6f19a7152eae..53c80a6194c5 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts @@ -26,6 +26,10 @@ describe('logger public API', () => { type: 'string', value: expect.any(String), }, + 'sentry.timestamp.sequence': { + type: 'integer', + value: expect.any(Number), + }, 'server.address': { type: 'string', value: expect.any(String), @@ -63,6 +67,10 @@ describe('logger public API', () => { type: 'string', value: expect.any(String), }, + 'sentry.timestamp.sequence': { + type: 'integer', + value: expect.any(Number), + }, 'server.address': { type: 'string', value: expect.any(String), @@ -100,6 +108,10 @@ describe('logger public API', () => { type: 'string', value: expect.any(String), }, + 'sentry.timestamp.sequence': { + type: 'integer', + value: expect.any(Number), + }, 'server.address': { type: 'string', value: expect.any(String), diff --git a/dev-packages/node-integration-tests/suites/public-api/logger/test.ts b/dev-packages/node-integration-tests/suites/public-api/logger/test.ts index f4be1cccc84b..6b9f43e738d2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/logger/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/logger/test.ts @@ -19,6 +19,10 @@ const commonAttributes: SerializedLog['attributes'] = { type: 'string', value: expect.any(String), }, + 'sentry.timestamp.sequence': { + type: 'integer', + value: expect.any(Number), + }, 'server.address': { type: 'string', value: expect.any(String), diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts index 1ee4eda2de3e..048513da3c19 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts @@ -24,6 +24,7 @@ describe('metrics server.address', () => { 'sentry.environment': { value: 'test', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }, ], diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 3408b01a5f96..097ffbb6906e 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -10,6 +10,7 @@ import { isParameterizedString } from '../utils/is'; import { getCombinedScopeData } from '../utils/scopeData'; import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; +import { getSequenceAttribute } from '../utils/timestampSequence'; import { _getTraceInfoFromScope } from '../utils/trace-info'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; import { createLogEnvelope } from './envelope'; @@ -154,8 +155,11 @@ export function _INTERNAL_captureLog( const { level, message, attributes: logAttributes = {}, severityNumber } = log; + const timestamp = timestampInSeconds(); + const sequenceAttr = getSequenceAttribute(timestamp); + const serializedLog: SerializedLog = { - timestamp: timestampInSeconds(), + timestamp, level, body: message, trace_id: traceContext?.trace_id, @@ -163,6 +167,7 @@ export function _INTERNAL_captureLog( attributes: { ...serializeAttributes(scopeAttributes), ...serializeAttributes(logAttributes, true), + [sequenceAttr.key]: sequenceAttr.value, }, }; diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index bdd13d884967..0545414654ef 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -11,6 +11,7 @@ import { debug } from '../utils/debug-logger'; import { getCombinedScopeData } from '../utils/scopeData'; import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; +import { getSequenceAttribute } from '../utils/timestampSequence'; import { _getTraceInfoFromScope } from '../utils/trace-info'; import { createMetricEnvelope } from './envelope'; @@ -135,8 +136,11 @@ function _buildSerializedMetric( const traceId = span ? span.spanContext().traceId : traceContext?.trace_id; const spanId = span ? span.spanContext().spanId : undefined; + const timestamp = timestampInSeconds(); + const sequenceAttr = getSequenceAttribute(timestamp); + return { - timestamp: timestampInSeconds(), + timestamp, trace_id: traceId ?? '', span_id: spanId, name: metric.name, @@ -146,6 +150,7 @@ function _buildSerializedMetric( attributes: { ...serializeAttributes(scopeAttributes), ...serializeAttributes(metric.attributes, 'skip-undefined'), + [sequenceAttr.key]: sequenceAttr.value, }, }; } diff --git a/packages/core/src/utils/timestampSequence.ts b/packages/core/src/utils/timestampSequence.ts new file mode 100644 index 000000000000..d2755d7a0724 --- /dev/null +++ b/packages/core/src/utils/timestampSequence.ts @@ -0,0 +1,41 @@ +const SEQUENCE_ATTR_KEY = 'sentry.timestamp.sequence'; + +let _sequenceNumber = 0; +let _previousTimestampMs: number | undefined; + +/** + * Returns the `sentry.timestamp.sequence` attribute entry for a serialized telemetry item. + * + * The sequence number starts at 0 and increments by 1 for each item captured. + * It resets to 0 when the current item's integer millisecond timestamp differs + * from the previous item's integer millisecond timestamp. + * + * @param timestampInSeconds - The timestamp of the telemetry item in seconds. + */ +export function getSequenceAttribute(timestampInSeconds: number): { + key: string; + value: { value: number; type: 'integer' }; +} { + const nowMs = Math.floor(timestampInSeconds * 1000); + + if (_previousTimestampMs !== undefined && nowMs !== _previousTimestampMs) { + _sequenceNumber = 0; + } + + const value = _sequenceNumber; + _sequenceNumber++; + _previousTimestampMs = nowMs; + + return { + key: SEQUENCE_ATTR_KEY, + value: { value, type: 'integer' }, + }; +} + +/** + * Resets the sequence number state. Only exported for testing purposes. + */ +export function _INTERNAL_resetSequenceNumber(): void { + _sequenceNumber = 0; + _previousTimestampMs = undefined; +} diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 2eec7c64dcbc..360485f5ca84 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -1,13 +1,18 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fmt, Scope } from '../../../src'; import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_getLogBuffer } from '../../../src/logs/internal'; import type { Log } from '../../../src/types-hoist/log'; import * as loggerModule from '../../../src/utils/debug-logger'; +import * as timeModule from '../../../src/utils/time'; +import { _INTERNAL_resetSequenceNumber } from '../../../src/utils/timestampSequence'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; describe('_INTERNAL_captureLog', () => { + beforeEach(() => { + _INTERNAL_resetSequenceNumber(); + }); it('captures and sends logs', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); @@ -23,7 +28,9 @@ describe('_INTERNAL_captureLog', () => { timestamp: expect.any(Number), trace_id: expect.any(String), severity_number: 9, - attributes: {}, + attributes: { + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }, }), ); }); @@ -86,6 +93,7 @@ describe('_INTERNAL_captureLog', () => { value: 'test', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -117,6 +125,7 @@ describe('_INTERNAL_captureLog', () => { value: '7.0.0', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -168,6 +177,7 @@ describe('_INTERNAL_captureLog', () => { value: 'auth', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -219,6 +229,7 @@ describe('_INTERNAL_captureLog', () => { type: 'boolean', value: true, }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); }); @@ -278,6 +289,7 @@ describe('_INTERNAL_captureLog', () => { value: 'Sentry', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -290,7 +302,9 @@ describe('_INTERNAL_captureLog', () => { _INTERNAL_captureLog({ level: 'debug', message: fmt`User logged in` }, scope); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); }); it('processes logs through beforeSendLog when provided', () => { @@ -344,7 +358,6 @@ describe('_INTERNAL_captureLog', () => { value: true, type: 'boolean', }, - // during serialization, they're converted to the typed attribute format scope_1: { value: 'attribute_value', type: 'string', @@ -354,6 +367,7 @@ describe('_INTERNAL_captureLog', () => { unit: 'gigabytes', type: 'integer', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }, }), ); @@ -439,6 +453,7 @@ describe('_INTERNAL_captureLog', () => { value: 'sampled-replay-id', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -464,8 +479,9 @@ describe('_INTERNAL_captureLog', () => { expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - // Should not include sentry.replay_id attribute - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); }); it('includes replay ID for buffer mode sessions', () => { @@ -499,6 +515,7 @@ describe('_INTERNAL_captureLog', () => { value: true, type: 'boolean', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -514,8 +531,9 @@ describe('_INTERNAL_captureLog', () => { _INTERNAL_captureLog({ level: 'info', message: 'test log without replay' }, scope); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - // Should not include sentry.replay_id attribute - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); }); it('combines replay ID with other log attributes', () => { @@ -568,6 +586,7 @@ describe('_INTERNAL_captureLog', () => { value: 'test-replay-id', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -592,7 +611,9 @@ describe('_INTERNAL_captureLog', () => { _INTERNAL_captureLog({ level: 'info', message: `test log with replay returning ${returnValue}` }, scope); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); expect(logAttributes).not.toHaveProperty('sentry.replay_id'); }); }); @@ -626,6 +647,7 @@ describe('_INTERNAL_captureLog', () => { value: true, type: 'boolean', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -654,6 +676,7 @@ describe('_INTERNAL_captureLog', () => { value: 'session-replay-id', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); @@ -683,6 +706,7 @@ describe('_INTERNAL_captureLog', () => { value: 'stopped-replay-id', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); @@ -708,7 +732,9 @@ describe('_INTERNAL_captureLog', () => { expect(mockReplayIntegration.getRecordingMode).not.toHaveBeenCalled(); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); expect(logAttributes).not.toHaveProperty('sentry.replay_id'); expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); }); @@ -725,7 +751,9 @@ describe('_INTERNAL_captureLog', () => { _INTERNAL_captureLog({ level: 'info', message: 'test log without replay integration' }, scope); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); expect(logAttributes).not.toHaveProperty('sentry.replay_id'); expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); @@ -784,6 +812,7 @@ describe('_INTERNAL_captureLog', () => { value: true, type: 'boolean', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); }); @@ -819,6 +848,7 @@ describe('_INTERNAL_captureLog', () => { value: 'testuser', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -844,6 +874,7 @@ describe('_INTERNAL_captureLog', () => { value: '123', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -874,6 +905,7 @@ describe('_INTERNAL_captureLog', () => { value: 'testuser', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -891,7 +923,9 @@ describe('_INTERNAL_captureLog', () => { _INTERNAL_captureLog({ level: 'info', message: 'test log with empty user' }, scope); const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; - expect(logAttributes).toEqual({}); + expect(logAttributes).toEqual({ + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); }); it('combines user data with other log attributes', () => { @@ -945,6 +979,7 @@ describe('_INTERNAL_captureLog', () => { value: 'test', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -975,6 +1010,7 @@ describe('_INTERNAL_captureLog', () => { value: 'user@example.com', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -1018,6 +1054,7 @@ describe('_INTERNAL_captureLog', () => { value: 'user@example.com', // Only added because user.email wasn't already present type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); @@ -1066,6 +1103,7 @@ describe('_INTERNAL_captureLog', () => { value: 'scope-user', // Added from scope because not present type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, }); }); }); @@ -1126,6 +1164,101 @@ describe('_INTERNAL_captureLog', () => { value: '7.0.0', type: 'string', }, + 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' }, + }); + }); + + describe('sentry.timestamp.sequence', () => { + it('increments the sequence number across consecutive logs', () => { + vi.spyOn(timeModule, 'timestampInSeconds').mockReturnValue(1000.001); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog({ level: 'info', message: 'first' }, scope); + _INTERNAL_captureLog({ level: 'info', message: 'second' }, scope); + _INTERNAL_captureLog({ level: 'info', message: 'third' }, scope); + + const buffer = _INTERNAL_getLogBuffer(client); + expect(buffer?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); + expect(buffer?.[1]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 1, type: 'integer' }); + expect(buffer?.[2]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 2, type: 'integer' }); + + vi.restoreAllMocks(); + }); + + it('does not increment the sequence number for dropped logs', () => { + vi.spyOn(timeModule, 'timestampInSeconds').mockReturnValue(1000.001); + + const beforeSendLog = vi.fn().mockImplementation(log => { + if (log.message === 'drop me') { + return null; + } + return log; + }); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true, beforeSendLog }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog({ level: 'info', message: 'keep first' }, scope); + _INTERNAL_captureLog({ level: 'info', message: 'drop me' }, scope); + _INTERNAL_captureLog({ level: 'info', message: 'keep second' }, scope); + + const buffer = _INTERNAL_getLogBuffer(client); + expect(buffer).toHaveLength(2); + expect(buffer?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); + expect(buffer?.[1]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 1, type: 'integer' }); + + vi.restoreAllMocks(); + }); + + it('produces monotonically increasing sequence numbers within the same millisecond', () => { + vi.spyOn(timeModule, 'timestampInSeconds').mockReturnValue(1000.001); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const count = 50; + for (let i = 0; i < count; i++) { + _INTERNAL_captureLog({ level: 'info', message: `log ${i}` }, scope); + } + + const buffer = _INTERNAL_getLogBuffer(client)!; + expect(buffer).toHaveLength(count); + + for (let i = 1; i < count; i++) { + const prev = (buffer[i - 1]?.attributes?.['sentry.timestamp.sequence'] as { value: number }).value; + const curr = (buffer[i]?.attributes?.['sentry.timestamp.sequence'] as { value: number }).value; + expect(curr).toBe(prev + 1); + } + + vi.restoreAllMocks(); + }); + + it('resets the sequence number via _INTERNAL_resetSequenceNumber', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureLog({ level: 'info', message: 'first' }, scope); + + _INTERNAL_resetSequenceNumber(); + + const client2 = new TestClient(options); + const scope2 = new Scope(); + scope2.setClient(client2); + + _INTERNAL_captureLog({ level: 'info', message: 'after reset' }, scope2); + + const buffer2 = _INTERNAL_getLogBuffer(client2); + expect(buffer2?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); }); }); }); diff --git a/packages/core/test/lib/metrics/internal.test.ts b/packages/core/test/lib/metrics/internal.test.ts index 434f4b6c8289..a598f323067d 100644 --- a/packages/core/test/lib/metrics/internal.test.ts +++ b/packages/core/test/lib/metrics/internal.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Scope } from '../../../src'; import { _INTERNAL_captureMetric, @@ -7,11 +7,18 @@ import { } from '../../../src/metrics/internal'; import type { Metric } from '../../../src/types-hoist/metric'; import * as loggerModule from '../../../src/utils/debug-logger'; +import { _INTERNAL_resetSequenceNumber } from '../../../src/utils/timestampSequence'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; +const SEQUENCE_ATTR = { 'sentry.timestamp.sequence': { value: expect.any(Number), type: 'integer' } }; + describe('_INTERNAL_captureMetric', () => { + beforeEach(() => { + _INTERNAL_resetSequenceNumber(); + }); + it('captures and sends metrics', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); @@ -27,7 +34,7 @@ describe('_INTERNAL_captureMetric', () => { value: 1, timestamp: expect.any(Number), trace_id: expect.any(String), - attributes: {}, + attributes: { ...SEQUENCE_ATTR }, }), ); }); @@ -80,6 +87,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.release': { value: '1.0.0', type: 'string', @@ -110,6 +118,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string', @@ -160,6 +169,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, endpoint: { value: '/api/users', type: 'string', @@ -183,6 +193,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, scope_attribute_1: { value: 1, type: 'integer', @@ -213,6 +224,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'my-attribute': { value: 43, type: 'integer' }, }); }); @@ -286,6 +298,7 @@ describe('_INTERNAL_captureMetric', () => { expect.objectContaining({ name: 'modified.original.metric', attributes: { + ...SEQUENCE_ATTR, processed: { value: true, type: 'boolean', @@ -370,6 +383,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.replay_id': { value: 'sampled-replay-id', type: 'string', @@ -397,7 +411,7 @@ describe('_INTERNAL_captureMetric', () => { expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true); const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; - expect(metricAttributes).toEqual({}); + expect(metricAttributes).toEqual({ ...SEQUENCE_ATTR }); }); it('includes replay ID for buffer mode sessions', () => { @@ -422,6 +436,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.replay_id': { value: 'buffer-replay-id', type: 'string', @@ -445,7 +460,7 @@ describe('_INTERNAL_captureMetric', () => { _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope }); const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; - expect(metricAttributes).toEqual({}); + expect(metricAttributes).toEqual({ ...SEQUENCE_ATTR }); }); it('combines replay ID with other metric attributes', () => { @@ -479,6 +494,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, endpoint: { value: '/api/users', type: 'string', @@ -523,7 +539,7 @@ describe('_INTERNAL_captureMetric', () => { _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope }); const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; - expect(metricAttributes).toEqual({}); + expect(metricAttributes).toEqual({ ...SEQUENCE_ATTR }); expect(metricAttributes).not.toHaveProperty('sentry.replay_id'); }); }); @@ -549,6 +565,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.replay_id': { value: 'buffer-replay-id', type: 'string', @@ -581,6 +598,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.replay_id': { value: 'session-replay-id', type: 'string', @@ -610,6 +628,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'sentry.replay_id': { value: 'stopped-replay-id', type: 'string', @@ -639,7 +658,7 @@ describe('_INTERNAL_captureMetric', () => { expect(mockReplayIntegration.getRecordingMode).not.toHaveBeenCalled(); const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; - expect(metricAttributes).toEqual({}); + expect(metricAttributes).toEqual({ ...SEQUENCE_ATTR }); expect(metricAttributes).not.toHaveProperty('sentry.replay_id'); expect(metricAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); @@ -656,7 +675,7 @@ describe('_INTERNAL_captureMetric', () => { _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope }); const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; - expect(metricAttributes).toEqual({}); + expect(metricAttributes).toEqual({ ...SEQUENCE_ATTR }); expect(metricAttributes).not.toHaveProperty('sentry.replay_id'); expect(metricAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); @@ -692,6 +711,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, endpoint: { value: '/api/users', type: 'string', @@ -738,6 +758,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'user.id': { value: '123', type: 'string', @@ -769,6 +790,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'user.id': { value: '123', type: 'string', @@ -793,6 +815,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'user.email': { value: 'user@example.com', type: 'string', @@ -816,7 +839,7 @@ describe('_INTERNAL_captureMetric', () => { _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope }); const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; - expect(metricAttributes).toEqual({}); + expect(metricAttributes).toEqual({ ...SEQUENCE_ATTR }); }); it('combines user data with other metric attributes', () => { @@ -846,6 +869,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, endpoint: { value: '/api/users', type: 'string', @@ -890,6 +914,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'user.id': { value: 123, type: 'integer', @@ -928,6 +953,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'user.custom': { value: 'custom-value', type: 'string', @@ -971,6 +997,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'other.attr': { value: 'value', type: 'string', @@ -1027,6 +1054,7 @@ describe('_INTERNAL_captureMetric', () => { const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; expect(metricAttributes).toEqual({ + ...SEQUENCE_ATTR, 'user.custom': { value: 'preserved-value', type: 'string', @@ -1049,4 +1077,42 @@ describe('_INTERNAL_captureMetric', () => { }, }); }); + + describe('sentry.timestamp.sequence', () => { + it('increments the sequence number across consecutive metrics', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric({ type: 'counter', name: 'first', value: 1 }, { scope }); + _INTERNAL_captureMetric({ type: 'counter', name: 'second', value: 2 }, { scope }); + _INTERNAL_captureMetric({ type: 'counter', name: 'third', value: 3 }, { scope }); + + const buffer = _INTERNAL_getMetricBuffer(client); + expect(buffer?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); + expect(buffer?.[1]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 1, type: 'integer' }); + expect(buffer?.[2]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 2, type: 'integer' }); + }); + + it('resets the sequence number via _INTERNAL_resetSequenceNumber', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric({ type: 'counter', name: 'first', value: 1 }, { scope }); + + _INTERNAL_resetSequenceNumber(); + + const client2 = new TestClient(options); + const scope2 = new Scope(); + scope2.setClient(client2); + + _INTERNAL_captureMetric({ type: 'counter', name: 'after reset', value: 2 }, { scope: scope2 }); + + const buffer2 = _INTERNAL_getMetricBuffer(client2); + expect(buffer2?.[0]?.attributes?.['sentry.timestamp.sequence']).toEqual({ value: 0, type: 'integer' }); + }); + }); }); diff --git a/packages/core/test/lib/metrics/public-api.test.ts b/packages/core/test/lib/metrics/public-api.test.ts index df8ff49c5553..cc8052cb0152 100644 --- a/packages/core/test/lib/metrics/public-api.test.ts +++ b/packages/core/test/lib/metrics/public-api.test.ts @@ -77,6 +77,10 @@ describe('Metrics Public API', () => { value: 'GET', type: 'string', }, + 'sentry.timestamp.sequence': { + value: expect.any(Number), + type: 'integer', + }, status: { value: 200, type: 'integer', @@ -194,6 +198,10 @@ describe('Metrics Public API', () => { value: 'websocket', type: 'string', }, + 'sentry.timestamp.sequence': { + value: expect.any(Number), + type: 'integer', + }, }, }), ); @@ -284,6 +292,10 @@ describe('Metrics Public API', () => { value: 'async', type: 'string', }, + 'sentry.timestamp.sequence': { + value: expect.any(Number), + type: 'integer', + }, }, }), ); diff --git a/packages/core/test/lib/utils/timestampSequence.test.ts b/packages/core/test/lib/utils/timestampSequence.test.ts new file mode 100644 index 000000000000..0608bf296455 --- /dev/null +++ b/packages/core/test/lib/utils/timestampSequence.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { _INTERNAL_resetSequenceNumber, getSequenceAttribute } from '../../../src/utils/timestampSequence'; + +describe('getSequenceAttribute', () => { + beforeEach(() => { + _INTERNAL_resetSequenceNumber(); + }); + + it('returns the correct attribute key', () => { + const attr = getSequenceAttribute(1000.001); + expect(attr.key).toBe('sentry.timestamp.sequence'); + }); + + it('returns an integer type attribute', () => { + const attr = getSequenceAttribute(1000.001); + expect(attr.value.type).toBe('integer'); + }); + + it('starts at 0', () => { + const attr = getSequenceAttribute(1000.001); + expect(attr.value.value).toBe(0); + }); + + it('increments by 1 for each call within the same millisecond', () => { + const first = getSequenceAttribute(1000.001); + const second = getSequenceAttribute(1000.001); + const third = getSequenceAttribute(1000.001); + + expect(first.value.value).toBe(0); + expect(second.value.value).toBe(1); + expect(third.value.value).toBe(2); + }); + + it('resets to 0 when the integer millisecond changes', () => { + // Same millisecond (1000001ms) + expect(getSequenceAttribute(1000.001).value.value).toBe(0); + expect(getSequenceAttribute(1000.001).value.value).toBe(1); + + // Different millisecond (1000002ms) + expect(getSequenceAttribute(1000.002).value.value).toBe(0); + expect(getSequenceAttribute(1000.002).value.value).toBe(1); + }); + + it('does not reset when the fractional part changes but integer millisecond stays the same', () => { + // 1000001.0ms and 1000001.9ms both floor to 1000001ms + expect(getSequenceAttribute(1000.001).value.value).toBe(0); + expect(getSequenceAttribute(1000.0019).value.value).toBe(1); + }); + + it('resets via _INTERNAL_resetSequenceNumber', () => { + expect(getSequenceAttribute(1000.001).value.value).toBe(0); + expect(getSequenceAttribute(1000.001).value.value).toBe(1); + + _INTERNAL_resetSequenceNumber(); + + expect(getSequenceAttribute(1000.001).value.value).toBe(0); + }); + + it('resets to 0 after _INTERNAL_resetSequenceNumber even with same timestamp', () => { + getSequenceAttribute(1000.001); + getSequenceAttribute(1000.001); + + _INTERNAL_resetSequenceNumber(); + + // After reset, _previousTimestampMs is undefined, so it should start at 0 + const attr = getSequenceAttribute(1000.001); + expect(attr.value.value).toBe(0); + }); + + it('shares sequence across interleaved calls (monotonically increasing within same ms)', () => { + // Simulate interleaved log and metric captures at the same timestamp + const logSeq = getSequenceAttribute(1000.001); + const metricSeq = getSequenceAttribute(1000.001); + const logSeq2 = getSequenceAttribute(1000.001); + + expect(logSeq.value.value).toBe(0); + expect(metricSeq.value.value).toBe(1); + expect(logSeq2.value.value).toBe(2); + }); +});