Skip to content
Draft
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
9 changes: 6 additions & 3 deletions packages/core/src/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function attributeValueToTypedAttributeValue(
return { ...attributeValue, ...checkedUnit };
}

if (!useFallback) {
if (!useFallback || value === undefined) {
return;
}

Expand Down Expand Up @@ -113,9 +113,12 @@ export function attributeValueToTypedAttributeValue(
*
* @returns The serialized attributes.
*/
export function serializeAttributes<T>(attributes: RawAttributes<T>, fallback: boolean = false): Attributes {
export function serializeAttributes<T>(
attributes: RawAttributes<T> | undefined,
fallback: boolean = false,
): Attributes {
const serializedAttributes: Attributes = {};
for (const [key, value] of Object.entries(attributes)) {
for (const [key, value] of Object.entries(attributes ?? {})) {
const typedValue = attributeValueToTypedAttributeValue(value, fallback);
if (typedValue) {
serializedAttributes[key] = typedValue;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ export type {
MetricType,
SerializedMetric,
SerializedMetricContainer,
// eslint-disable-next-line deprecation/deprecation
SerializedMetricAttributeValue,
} from './types-hoist/metric';
export type { TimedEvent } from './types-hoist/timedEvent';
Expand Down
57 changes: 3 additions & 54 deletions packages/core/src/metrics/internal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { serializeAttributes } from '../attributes';
import { getGlobalSingleton } from '../carrier';
import type { Client } 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 type { Metric, SerializedMetric } from '../types-hoist/metric';
import { mergeScopeData } from '../utils/applyScopeDataToEvent';
import { debug } from '../utils/debug-logger';
import { _getSpanForScope } from '../utils/spanOnScope';
Expand All @@ -14,50 +15,6 @@ import { createMetricEnvelope } from './envelope';

const MAX_METRIC_BUFFER_SIZE = 1000;

/**
* Converts a metric attribute to a serialized metric attribute.
*
* @param value - The value of the metric attribute.
* @returns The serialized metric attribute.
*/
export function metricAttributeToSerializedMetricAttribute(value: unknown): SerializedMetricAttributeValue {
switch (typeof value) {
case 'number':
if (Number.isInteger(value)) {
return {
value,
type: 'integer',
};
}
return {
value,
type: 'double',
};
case 'boolean':
return {
value,
type: 'boolean',
};
case 'string':
return {
value,
type: 'string',
};
default: {
let stringValue = '';
try {
stringValue = JSON.stringify(value) ?? '';
} catch {
// Do nothing
}
return {
value: stringValue,
type: 'string',
};
}
}
}

/**
* Sets a metric attribute if the value exists and the attribute key is not already present.
*
Expand Down Expand Up @@ -169,14 +126,6 @@ function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentSc
* Creates a serialized metric ready to be sent to Sentry.
*/
function _buildSerializedMetric(metric: Metric, client: Client, currentScope: Scope): SerializedMetric {
// Serialize attributes
const serializedAttributes: Record<string, SerializedMetricAttributeValue> = {};
for (const key in metric.attributes) {
if (metric.attributes[key] !== undefined) {
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(metric.attributes[key]);
}
}

// Get trace context
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
const span = _getSpanForScope(currentScope);
Expand All @@ -191,7 +140,7 @@ function _buildSerializedMetric(metric: Metric, client: Client, currentScope: Sc
type: metric.type,
unit: metric.unit,
value: metric.value,
attributes: serializedAttributes,
attributes: serializeAttributes(metric.attributes, true),
};
}

Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/types-hoist/metric.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Attributes, TypedAttributeValue } from '../attributes';

export type MetricType = 'counter' | 'gauge' | 'distribution';

export interface Metric {
Expand Down Expand Up @@ -27,11 +29,10 @@ export interface Metric {
attributes?: Record<string, unknown>;
}

export type SerializedMetricAttributeValue =
| { value: string; type: 'string' }
| { value: number; type: 'integer' }
| { value: number; type: 'double' }
| { value: boolean; type: 'boolean' };
/**
* @deprecated this was not intended for public consumption
*/
export type SerializedMetricAttributeValue = TypedAttributeValue;

export interface SerializedMetric {
/**
Expand Down Expand Up @@ -72,7 +73,7 @@ export interface SerializedMetric {
/**
* Arbitrary structured data that stores information about the metric.
*/
attributes?: Record<string, SerializedMetricAttributeValue>;
attributes?: Attributes;
}

export type SerializedMetricContainer = {
Expand Down
103 changes: 102 additions & 1 deletion packages/core/test/lib/attributes.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { attributeValueToTypedAttributeValue, isAttributeObject } from '../../src/attributes';
import { attributeValueToTypedAttributeValue, isAttributeObject, serializeAttributes } from '../../src/attributes';

describe('attributeValueToTypedAttributeValue', () => {
describe('without fallback (default behavior)', () => {
Expand Down Expand Up @@ -267,6 +267,19 @@ describe('attributeValueToTypedAttributeValue', () => {
type: 'string',
});
});

it.each([null, { value: null }, { value: null, unit: 'byte' }])('stringifies %s values', value => {
const result = attributeValueToTypedAttributeValue(value, true);
expect(result).toMatchObject({
value: 'null',
type: 'string',
});
});

it.each([undefined, { value: undefined }, { value: undefined, unit: 'byte' }])('ignores %s values', value => {
const result = attributeValueToTypedAttributeValue(value, true);
expect(result).toBeUndefined();
});
});
});
});
Expand Down Expand Up @@ -297,3 +310,91 @@ describe('isAttributeObject', () => {
},
);
});

describe('serializeAttributes', () => {
it('returns an empty object for undefined attributes', () => {
const result = serializeAttributes(undefined);
expect(result).toStrictEqual({});
});

it('returns an empty object for an empty object', () => {
const result = serializeAttributes({});
expect(result).toStrictEqual({});
});

it('serializes valid, non-primitive values', () => {
const result = serializeAttributes({ foo: 'bar', bar: { value: 123 }, baz: { value: 456, unit: 'byte' } });
expect(result).toStrictEqual({
bar: {
type: 'integer',
value: 123,
},
baz: {
type: 'integer',
unit: 'byte',
value: 456,
},
foo: {
type: 'string',
value: 'bar',
},
});
});

it.each([true, false])('ignores undefined values if fallback is %s', fallback => {
const result = serializeAttributes(
{ foo: undefined, bar: { value: undefined }, baz: { value: undefined, unit: 'byte' } },
fallback,
);
expect(result).toStrictEqual({});
});

it('ignores null values by default', () => {
const result = serializeAttributes({ foo: null, bar: { value: null }, baz: { value: null, unit: 'byte' } });
expect(result).toStrictEqual({});
});

it('stringifies to `"null"` if fallback is true', () => {
const result = serializeAttributes({ foo: null, bar: { value: null }, baz: { value: null, unit: 'byte' } }, true);
expect(result).toStrictEqual({
foo: {
type: 'string',
value: 'null',
},
bar: {
type: 'string',
value: 'null',
},
baz: {
type: 'string',
unit: 'byte',
value: 'null',
},
});
});

describe('invalid (non-primitive) values', () => {
it("doesn't fall back to stringification by default", () => {
const result = serializeAttributes({ foo: { some: 'object' }, bar: [1, 2, 3], baz: () => {} });
expect(result).toStrictEqual({});
});

it('falls back to stringification of unsupported non-primitive values if fallback is true', () => {
const result = serializeAttributes({ foo: { some: 'object' }, bar: [1, 2, 3], baz: () => {} }, true);
expect(result).toStrictEqual({
bar: {
type: 'string',
value: '[1,2,3]',
},
baz: {
type: 'string',
value: '',
},
foo: {
type: 'string',
value: '{"some":"object"}',
},
});
});
});
});
69 changes: 0 additions & 69 deletions packages/core/test/lib/metrics/internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,13 @@ import {
_INTERNAL_captureMetric,
_INTERNAL_flushMetricsBuffer,
_INTERNAL_getMetricBuffer,
metricAttributeToSerializedMetricAttribute,
} from '../../../src/metrics/internal';
import type { Metric } from '../../../src/types-hoist/metric';
import * as loggerModule from '../../../src/utils/debug-logger';
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';

const PUBLIC_DSN = 'https://username@domain/123';

describe('metricAttributeToSerializedMetricAttribute', () => {
it('serializes integer values', () => {
const result = metricAttributeToSerializedMetricAttribute(42);
expect(result).toEqual({
value: 42,
type: 'integer',
});
});

it('serializes double values', () => {
const result = metricAttributeToSerializedMetricAttribute(42.34);
expect(result).toEqual({
value: 42.34,
type: 'double',
});
});

it('serializes boolean values', () => {
const result = metricAttributeToSerializedMetricAttribute(true);
expect(result).toEqual({
value: true,
type: 'boolean',
});
});

it('serializes string values', () => {
const result = metricAttributeToSerializedMetricAttribute('endpoint');
expect(result).toEqual({
value: 'endpoint',
type: 'string',
});
});

it('serializes object values as JSON strings', () => {
const obj = { name: 'John', age: 30 };
const result = metricAttributeToSerializedMetricAttribute(obj);
expect(result).toEqual({
value: JSON.stringify(obj),
type: 'string',
});
});

it('serializes array values as JSON strings', () => {
const array = [1, 2, 3, 'test'];
const result = metricAttributeToSerializedMetricAttribute(array);
expect(result).toEqual({
value: JSON.stringify(array),
type: 'string',
});
});

it('serializes undefined values as empty strings', () => {
const result = metricAttributeToSerializedMetricAttribute(undefined);
expect(result).toEqual({
value: '',
type: 'string',
});
});

it('serializes null values as JSON strings', () => {
const result = metricAttributeToSerializedMetricAttribute(null);
expect(result).toEqual({
value: 'null',
type: 'string',
});
});
});

describe('_INTERNAL_captureMetric', () => {
it('captures and sends metrics', () => {
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
Expand Down
Loading