Skip to content

Commit 6f3c061

Browse files
authored
Merge branch 'develop' into feat/node-openai-azure-support
2 parents e7a85f4 + e8a1826 commit 6f3c061

File tree

13 files changed

+949
-120
lines changed

13 files changed

+949
-120
lines changed

.size-limit.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = [
3838
path: 'packages/browser/build/npm/esm/prod/index.js',
3939
import: createImport('init', 'browserTracingIntegration'),
4040
gzip: true,
41-
limit: '41.5 KB',
41+
limit: '42 KB',
4242
},
4343
{
4444
name: '@sentry/browser (incl. Tracing, Profiling)',
@@ -127,7 +127,7 @@ module.exports = [
127127
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
128128
ignore: ['react/jsx-runtime'],
129129
gzip: true,
130-
limit: '43.5 KB',
130+
limit: '44 KB',
131131
},
132132
// Vue SDK (ESM)
133133
{
@@ -142,7 +142,7 @@ module.exports = [
142142
path: 'packages/vue/build/esm/index.js',
143143
import: createImport('init', 'browserTracingIntegration'),
144144
gzip: true,
145-
limit: '43.3 KB',
145+
limit: '44 KB',
146146
},
147147
// Svelte SDK (ESM)
148148
{
@@ -163,7 +163,7 @@ module.exports = [
163163
name: 'CDN Bundle (incl. Tracing)',
164164
path: createCDNPath('bundle.tracing.min.js'),
165165
gzip: true,
166-
limit: '42.1 KB',
166+
limit: '42.5 KB',
167167
},
168168
{
169169
name: 'CDN Bundle (incl. Tracing, Replay)',
@@ -183,14 +183,14 @@ module.exports = [
183183
path: createCDNPath('bundle.min.js'),
184184
gzip: false,
185185
brotli: false,
186-
limit: '80 KB',
186+
limit: '82 KB',
187187
},
188188
{
189189
name: 'CDN Bundle (incl. Tracing) - uncompressed',
190190
path: createCDNPath('bundle.tracing.min.js'),
191191
gzip: false,
192192
brotli: false,
193-
limit: '125 KB',
193+
limit: '127 KB',
194194
},
195195
{
196196
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
@@ -231,7 +231,7 @@ module.exports = [
231231
import: createImport('init'),
232232
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
233233
gzip: true,
234-
limit: '51.1 KB',
234+
limit: '52 KB',
235235
},
236236
// Node SDK (ESM)
237237
{

dev-packages/e2e-tests/test-applications/cloudflare-astro/astro.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const dsn = process.env.E2E_TEST_DSN;
66

77
// https://astro.build/config
88
export default defineConfig({
9-
output: 'hybrid',
109
adapter: cloudflare({
1110
imageService: 'passthrough',
1211
}),

dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"test:assert": "pnpm -v"
1818
},
1919
"dependencies": {
20-
"@astrojs/cloudflare": "8.1.0",
20+
"@astrojs/cloudflare": "12.6.11",
2121
"@sentry/astro": "latest || *",
2222
"astro": "5.15.9"
2323
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable no-unused-vars */
2+
3+
const Sentry = require('@sentry/node');
4+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
5+
6+
const externalFunctionFile = require.resolve('./node_modules/out-of-app-function.js');
7+
8+
const { out_of_app_function } = require(externalFunctionFile);
9+
10+
function in_app_function() {
11+
const inAppVar = 'in app value';
12+
out_of_app_function(`${inAppVar} modified value`);
13+
}
14+
15+
Sentry.init({
16+
dsn: 'https://[email protected]/1337',
17+
transport: loggingTransport,
18+
includeLocalVariables: true,
19+
});
20+
21+
setTimeout(async () => {
22+
try {
23+
in_app_function();
24+
} catch (e) {
25+
Sentry.captureException(e);
26+
await Sentry.flush();
27+
}
28+
}, 500);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* eslint-disable no-unused-vars */
2+
3+
const Sentry = require('@sentry/node');
4+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
5+
6+
const externalFunctionFile = require.resolve('./node_modules/out-of-app-function.js');
7+
8+
const { out_of_app_function } = require(externalFunctionFile);
9+
10+
Sentry.init({
11+
dsn: 'https://[email protected]/1337',
12+
transport: loggingTransport,
13+
includeLocalVariables: true,
14+
integrations: [
15+
Sentry.localVariablesIntegration({
16+
includeOutOfAppFrames: true,
17+
}),
18+
],
19+
});
20+
21+
function in_app_function() {
22+
const inAppVar = 'in app value';
23+
out_of_app_function(`${inAppVar} modified value`);
24+
}
25+
26+
setTimeout(async () => {
27+
try {
28+
in_app_function();
29+
} catch (e) {
30+
Sentry.captureException(e);
31+
await Sentry.flush();
32+
}
33+
}, 500);

dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { mkdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs';
12
import * as path from 'path';
2-
import { afterAll, describe, expect, test } from 'vitest';
3+
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
34
import { conditionalTest } from '../../../utils';
45
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
56

@@ -39,8 +40,35 @@ const EXPECTED_LOCAL_VARIABLES_EVENT = {
3940
};
4041

4142
describe('LocalVariables integration', () => {
43+
const nodeModules = `${__dirname}/node_modules`;
44+
const externalModule = `${nodeModules}//out-of-app-function.js`;
45+
function cleanupExternalModuleFile() {
46+
try {
47+
unlinkSync(externalModule);
48+
// eslint-disable-next-line no-empty
49+
} catch {}
50+
try {
51+
rmdirSync(nodeModules);
52+
// eslint-disable-next-line no-empty
53+
} catch {}
54+
}
55+
56+
beforeAll(() => {
57+
cleanupExternalModuleFile();
58+
mkdirSync(nodeModules, { recursive: true });
59+
writeFileSync(
60+
externalModule,
61+
`
62+
function out_of_app_function(passedArg) {
63+
const outOfAppVar = "out of app value " + passedArg.substring(13);
64+
throw new Error("out-of-app error");
65+
}
66+
module.exports = { out_of_app_function };`,
67+
);
68+
});
4269
afterAll(() => {
4370
cleanupChildProcesses();
71+
cleanupExternalModuleFile();
4472
});
4573

4674
test('Should not include local variables by default', async () => {
@@ -127,4 +155,47 @@ describe('LocalVariables integration', () => {
127155
.start()
128156
.completed();
129157
});
158+
159+
test('adds local variables to out of app frames when includeOutOfAppFrames is true', async () => {
160+
await createRunner(__dirname, 'local-variables-out-of-app.js')
161+
.expect({
162+
event: event => {
163+
const frames = event.exception?.values?.[0]?.stacktrace?.frames || [];
164+
165+
const inAppFrame = frames.find(frame => frame.function === 'in_app_function');
166+
const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function');
167+
168+
expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' });
169+
expect(inAppFrame?.in_app).toEqual(true);
170+
171+
expect(outOfAppFrame?.vars).toEqual({
172+
outOfAppVar: 'out of app value modified value',
173+
passedArg: 'in app value modified value',
174+
});
175+
expect(outOfAppFrame?.in_app).toEqual(false);
176+
},
177+
})
178+
.start()
179+
.completed();
180+
});
181+
182+
test('does not add local variables to out of app frames by default', async () => {
183+
await createRunner(__dirname, 'local-variables-out-of-app-default.js')
184+
.expect({
185+
event: event => {
186+
const frames = event.exception?.values?.[0]?.stacktrace?.frames || [];
187+
188+
const inAppFrame = frames.find(frame => frame.function === 'in_app_function');
189+
const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function');
190+
191+
expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' });
192+
expect(inAppFrame?.in_app).toEqual(true);
193+
194+
expect(outOfAppFrame?.vars).toBeUndefined();
195+
expect(outOfAppFrame?.in_app).toEqual(false);
196+
},
197+
})
198+
.start()
199+
.completed();
200+
});
130201
});

packages/core/src/attributes.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { DEBUG_BUILD } from './debug-build';
2+
import type { DurationUnit, FractionUnit, InformationUnit } from './types-hoist/measurement';
3+
import { debug } from './utils/debug-logger';
4+
5+
export type RawAttributes<T> = T & ValidatedAttributes<T>;
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
export type RawAttribute<T> = T extends { value: any } | { unit: any } ? AttributeObject : T;
8+
9+
export type Attributes = Record<string, TypedAttributeValue>;
10+
11+
export type AttributeValueType = string | number | boolean | Array<string> | Array<number> | Array<boolean>;
12+
13+
type AttributeTypeMap = {
14+
string: string;
15+
integer: number;
16+
double: number;
17+
boolean: boolean;
18+
'string[]': Array<string>;
19+
'integer[]': Array<number>;
20+
'double[]': Array<number>;
21+
'boolean[]': Array<boolean>;
22+
};
23+
24+
/* Generates a type from the AttributeTypeMap like:
25+
| { value: string; type: 'string' }
26+
| { value: number; type: 'integer' }
27+
| { value: number; type: 'double' }
28+
*/
29+
type AttributeUnion = {
30+
[K in keyof AttributeTypeMap]: {
31+
value: AttributeTypeMap[K];
32+
type: K;
33+
};
34+
}[keyof AttributeTypeMap];
35+
36+
export type TypedAttributeValue = AttributeUnion & { unit?: AttributeUnit };
37+
38+
export type AttributeObject = {
39+
value: unknown;
40+
unit?: AttributeUnit;
41+
};
42+
43+
// Unfortunately, we loose type safety if we did something like Exclude<MeasurementUnit, string>
44+
// so therefore we unionize between the three supported unit categories.
45+
type AttributeUnit = DurationUnit | InformationUnit | FractionUnit;
46+
47+
/* If an attribute has either a 'value' or 'unit' property, we use the ValidAttributeObject type. */
48+
export type ValidatedAttributes<T> = {
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50+
[K in keyof T]: T[K] extends { value: any } | { unit: any } ? AttributeObject : unknown;
51+
};
52+
53+
/**
54+
* Type-guard: The attribute object has the shape the official attribute object (value, type, unit).
55+
* https://develop.sentry.dev/sdk/telemetry/scopes/#setting-attributes
56+
*/
57+
export function isAttributeObject(maybeObj: unknown): maybeObj is AttributeObject {
58+
return (
59+
typeof maybeObj === 'object' &&
60+
maybeObj != null &&
61+
!Array.isArray(maybeObj) &&
62+
Object.keys(maybeObj).includes('value')
63+
);
64+
}
65+
66+
/**
67+
* Converts an attribute value to a typed attribute value.
68+
*
69+
* Does not allow mixed arrays. In case of a mixed array, the value is stringified and the type is 'string'.
70+
* All values besides the supported attribute types (see {@link AttributeTypeMap}) are stringified to a string attribute value.
71+
*
72+
* @param value - The value of the passed attribute.
73+
* @returns The typed attribute.
74+
*/
75+
export function attributeValueToTypedAttributeValue(rawValue: unknown): TypedAttributeValue {
76+
const { value, unit } = isAttributeObject(rawValue) ? rawValue : { value: rawValue, unit: undefined };
77+
return { ...getTypedAttributeValue(value), ...(unit && typeof unit === 'string' ? { unit } : {}) };
78+
}
79+
80+
// Only allow string, boolean, or number types
81+
const getPrimitiveType: (
82+
item: unknown,
83+
) => keyof Pick<AttributeTypeMap, 'string' | 'integer' | 'double' | 'boolean'> | null = item =>
84+
typeof item === 'string'
85+
? 'string'
86+
: typeof item === 'boolean'
87+
? 'boolean'
88+
: typeof item === 'number' && !Number.isNaN(item)
89+
? Number.isInteger(item)
90+
? 'integer'
91+
: 'double'
92+
: null;
93+
94+
function getTypedAttributeValue(value: unknown): TypedAttributeValue {
95+
const primitiveType = getPrimitiveType(value);
96+
if (primitiveType) {
97+
// @ts-expect-error - TS complains because {@link TypedAttributeValue} is strictly typed to
98+
// avoid setting the wrong `type` on the attribute value.
99+
// In this case, getPrimitiveType already does the check but TS doesn't know that.
100+
// The "clean" alternative is to return an object per `typeof value` case
101+
// but that would require more bundle size
102+
// Therefore, we ignore it.
103+
return { value, type: primitiveType };
104+
}
105+
106+
if (Array.isArray(value)) {
107+
const coherentArrayType = value.reduce((acc: 'string' | 'boolean' | 'integer' | 'double' | null, item) => {
108+
if (!acc || getPrimitiveType(item) !== acc) {
109+
return null;
110+
}
111+
return acc;
112+
}, getPrimitiveType(value[0]));
113+
114+
if (coherentArrayType) {
115+
return { value, type: `${coherentArrayType}[]` };
116+
}
117+
}
118+
119+
// Fallback: stringify the passed value
120+
let fallbackValue = '';
121+
try {
122+
fallbackValue = JSON.stringify(value) ?? String(value);
123+
} catch {
124+
try {
125+
fallbackValue = String(value);
126+
} catch {
127+
DEBUG_BUILD && debug.warn('Failed to stringify attribute value', value);
128+
// ignore
129+
}
130+
}
131+
132+
// This is quite a low-quality message but we cannot safely log the original `value`
133+
// here due to String() or JSON.stringify() potentially throwing.
134+
DEBUG_BUILD &&
135+
debug.log(`Stringified attribute value to ${fallbackValue} because it's not a supported attribute value type`);
136+
137+
return {
138+
value: fallbackValue,
139+
type: 'string',
140+
};
141+
}

0 commit comments

Comments
 (0)