From 1ef780751a3fd895ada82164344e1aa7e9e0acb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 19:33:14 +0000 Subject: [PATCH 01/25] feat(deps): Bump @prisma/instrumentation from 6.8.2 to 6.9.0 (#16608) --- packages/node/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 630d79f25594..b4cad6b21696 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -94,7 +94,7 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@prisma/instrumentation": "6.8.2", + "@prisma/instrumentation": "6.9.0", "@sentry/core": "9.29.0", "@sentry/opentelemetry": "9.29.0", "import-in-the-middle": "^1.13.1", diff --git a/yarn.lock b/yarn.lock index 0deb47ad0828..ce3e50da8c41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5878,10 +5878,10 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== -"@prisma/instrumentation@6.8.2": - version "6.8.2" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.8.2.tgz#77a87a37f67ab35eaaf8ff629f889e9e11a465ac" - integrity sha512-5NCTbZjw7a+WIZ/ey6G8SY+YKcyM2zBF0hOT1muvqC9TbVtTCr5Qv3RL/2iNDOzLUHEvo4I1uEfioyfuNOGK8Q== +"@prisma/instrumentation@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.9.0.tgz#2de2def755e39847fc1a38687cfa92a3f115b2b1" + integrity sha512-HFfr89v7WEbygdTzh1t171SUMYlkFRTXf48QthDc1cKduEsGIsOdt1QhOlpF7VK+yMg9EXHaXQo5Z8lQ7WtEYA== dependencies: "@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" From d302703f59cc65b711b3eb213ca962db73b83576 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:32:51 +0200 Subject: [PATCH 02/25] test(deps): Bump next from 14.1.3 to 14.2.30 in /dev-packages/e2e-tests/test-applications/nextjs-14 (#16583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [next](https://github.com/vercel/next.js) from 14.1.3 to 14.2.30.
Release notes

Sourced from next's releases.

v14.2.30

[!NOTE]
This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

Credits

Huge thanks to @​ijjk and @​ztanner for helping!

v14.2.29

[!NOTE]
This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

Credits

Huge thanks to @​ijjk for helping!

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=14.1.3&new-version=14.2.30)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/e2e-tests/test-applications/nextjs-14/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index a1e33dbb10ec..a7365cc3fb10 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -17,7 +17,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "14.1.3", + "next": "14.2.30", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "~5.0.0" From cb5265c53cea2a00ffbface881dcde493f784452 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Tue, 17 Jun 2025 11:58:25 +0200 Subject: [PATCH 03/25] ref(node): Improve span flushing (#16577) Follow up to https://github.com/getsentry/sentry-javascript/pull/16416, slightly moving things around and making some small improvements to performance and logic (some not related to that PR but another improvement we noticed when BYK looked int this): 1. Actually debounce the flushing properly, including a maxWait of 100ms. Previously, it was technically possible to debounce flushing forever, if a span was flushed every ms. Now, at least after 100ms it should _always_ flush. 2. Simplify overhead by avoid checking for the 5min timeout of sent spans. As this check `isSpanAlreadySent` has been called for every single span that ended, it is quite high impact, and meant a little bit of additional work (although it should not necessarily run _too_ often, but still). Instead, we now simply check for `map.has(id)` which should be good enough for what we want to achieve, IMHO. We still clean up the sent spans when flushing, so old stuff should still go away eventually. 3. Made stuff that is not needed as public API private on the span exporter class. this is technically breaking but I think it is OK, this should not be public API surface as it does not need to be called from outside. For this I moved the already existing `debounce` function from replay to core. We re-implement this in replay with a different setTimeout impl. which is needed for some angular stuff. --------- Co-authored-by: Abhijeet Prasad --- packages/core/src/index.ts | 1 + packages/core/src/utils/debounce.ts | 76 +++++ packages/core/test/lib/utils/debounce.test.ts | 276 ++++++++++++++++++ packages/opentelemetry/src/spanExporter.ts | 117 ++++---- packages/replay-internal/src/util/debounce.ts | 48 +-- 5 files changed, 408 insertions(+), 110 deletions(-) create mode 100644 packages/core/src/utils/debounce.ts create mode 100644 packages/core/test/lib/utils/debounce.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 986d18a972d2..d418b8a28c19 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -88,6 +88,7 @@ export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; export { getTraceData } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; +export { debounce } from './utils/debounce'; export { winterCGHeadersToDict, winterCGRequestToRequestData, diff --git a/packages/core/src/utils/debounce.ts b/packages/core/src/utils/debounce.ts new file mode 100644 index 000000000000..5f936d1e14e2 --- /dev/null +++ b/packages/core/src/utils/debounce.ts @@ -0,0 +1,76 @@ +type DebouncedCallback = { + (): void | unknown; + flush: () => void | unknown; + cancel: () => void; +}; +type CallbackFunction = () => unknown; +type DebounceOptions = { + /** The max. time in ms to wait for the callback to be invoked. */ + maxWait?: number; + /** This can be overwritten to use a different setTimeout implementation, e.g. to avoid triggering change detection in Angular */ + setTimeoutImpl?: typeof setTimeout; +}; + +/** + * Heavily simplified debounce function based on lodash.debounce. + * + * This function takes a callback function (@param fun) and delays its invocation + * by @param wait milliseconds. Optionally, a maxWait can be specified in @param options, + * which ensures that the callback is invoked at least once after the specified max. wait time. + * + * @param func the function whose invocation is to be debounced + * @param wait the minimum time until the function is invoked after it was called once + * @param options the options object, which can contain the `maxWait` property + * + * @returns the debounced version of the function, which needs to be called at least once to start the + * debouncing process. Subsequent calls will reset the debouncing timer and, in case @paramfunc + * was already invoked in the meantime, return @param func's return value. + * The debounced function has two additional properties: + * - `flush`: Invokes the debounced function immediately and returns its return value + * - `cancel`: Cancels the debouncing process and resets the debouncing timer + */ +export function debounce(func: CallbackFunction, wait: number, options?: DebounceOptions): DebouncedCallback { + let callbackReturnValue: unknown; + + let timerId: ReturnType | undefined; + let maxTimerId: ReturnType | undefined; + + const maxWait = options?.maxWait ? Math.max(options.maxWait, wait) : 0; + const setTimeoutImpl = options?.setTimeoutImpl || setTimeout; + + function invokeFunc(): unknown { + cancelTimers(); + callbackReturnValue = func(); + return callbackReturnValue; + } + + function cancelTimers(): void { + timerId !== undefined && clearTimeout(timerId); + maxTimerId !== undefined && clearTimeout(maxTimerId); + timerId = maxTimerId = undefined; + } + + function flush(): unknown { + if (timerId !== undefined || maxTimerId !== undefined) { + return invokeFunc(); + } + return callbackReturnValue; + } + + function debounced(): unknown { + if (timerId) { + clearTimeout(timerId); + } + timerId = setTimeoutImpl(invokeFunc, wait); + + if (maxWait && maxTimerId === undefined) { + maxTimerId = setTimeoutImpl(invokeFunc, maxWait); + } + + return callbackReturnValue; + } + + debounced.cancel = cancelTimers; + debounced.flush = flush; + return debounced; +} diff --git a/packages/core/test/lib/utils/debounce.test.ts b/packages/core/test/lib/utils/debounce.test.ts new file mode 100644 index 000000000000..d44371e94b49 --- /dev/null +++ b/packages/core/test/lib/utils/debounce.test.ts @@ -0,0 +1,276 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { debounce } from '../../../src/utils/debounce'; + +describe('Unit | util | debounce', () => { + beforeAll(() => { + vi.useFakeTimers(); + }); + + it('delay the execution of the passed callback function by the passed minDelay', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(1); + expect(callback).toHaveBeenCalled(); + }); + + it('should invoke the callback at latest by maxWait, if the option is specified', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100, { maxWait: 150 }); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(98); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback(); + + vi.advanceTimersByTime(1); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(49); + // at this time, the callback shouldn't be invoked and with a new call, it should be debounced further. + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + // But because the maxWait is reached, the callback should nevertheless be invoked. + vi.advanceTimersByTime(10); + expect(callback).toHaveBeenCalled(); + }); + + it('should not invoke the callback as long as it is debounced and no maxWait option is specified', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback(); + + vi.advanceTimersByTime(1); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(98); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + debouncedCallback(); + + vi.advanceTimersByTime(100); + expect(callback).toHaveBeenCalled(); + }); + + it('should invoke the callback as soon as callback.flush() is called', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100, { maxWait: 200 }); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(10); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback.flush(); + expect(callback).toHaveBeenCalled(); + }); + + it('should not invoke the callback, if callback.cancel() is called', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100, { maxWait: 200 }); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + + // If the callback is canceled, it should not be invoked after the minwait + debouncedCallback.cancel(); + vi.advanceTimersByTime(1); + expect(callback).not.toHaveBeenCalled(); + + // And it should also not be invoked after the maxWait + vi.advanceTimersByTime(500); + expect(callback).not.toHaveBeenCalled(); + }); + + it("should return the callback's return value when calling callback.flush()", () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100); + + debouncedCallback(); + + const returnValue = debouncedCallback.flush(); + expect(returnValue).toBe('foo'); + }); + + it('should return the callbacks return value on subsequent calls of the debounced function', () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100); + + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(200); + expect(callback).toHaveBeenCalledTimes(1); + + // calling the debounced function now should return the return value of the callback execution + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe('foo'); + expect(callback).toHaveBeenCalledTimes(1); + + // and the callback should also be invoked again + vi.advanceTimersByTime(200); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should handle return values of consecutive invocations without maxWait', () => { + let i = 0; + const callback = vi.fn().mockImplementation(() => { + return `foo-${++i}`; + }); + const debouncedCallback = debounce(callback, 100); + + const returnValue0 = debouncedCallback(); + expect(returnValue0).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(200); + expect(callback).toHaveBeenCalledTimes(1); + + // calling the debounced function now should return the return value of the callback execution + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(1); + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + // and the callback should also be invoked again + vi.advanceTimersByTime(200); + const returnValue3 = debouncedCallback(); + expect(returnValue3).toBe('foo-2'); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should handle return values of consecutive invocations with maxWait', () => { + let i = 0; + const callback = vi.fn().mockImplementation(() => { + return `foo-${++i}`; + }); + const debouncedCallback = debounce(callback, 150, { maxWait: 200 }); + + const returnValue0 = debouncedCallback(); + expect(returnValue0).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(149); + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // calling the debounced function now should return the return value of the callback execution + // as it was executed because of maxWait + vi.advanceTimersByTime(51); + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + // at this point (100ms after the last debounce call), nothing should have happened + vi.advanceTimersByTime(100); + const returnValue3 = debouncedCallback(); + expect(returnValue3).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + // and the callback should now have been invoked again + vi.advanceTimersByTime(150); + const returnValue4 = debouncedCallback(); + expect(returnValue4).toBe('foo-2'); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should handle return values of consecutive invocations after a cancellation', () => { + let i = 0; + const callback = vi.fn().mockImplementation(() => { + return `foo-${++i}`; + }); + const debouncedCallback = debounce(callback, 150, { maxWait: 200 }); + + const returnValue0 = debouncedCallback(); + expect(returnValue0).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(149); + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback.cancel(); + + // calling the debounced function now still return undefined because we cancelled the invocation + vi.advanceTimersByTime(51); + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // and the callback should also be invoked again + vi.advanceTimersByTime(150); + const returnValue3 = debouncedCallback(); + expect(returnValue3).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should handle the return value of calling flush after cancelling', () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100); + + debouncedCallback(); + debouncedCallback.cancel(); + + const returnValue = debouncedCallback.flush(); + expect(returnValue).toBe(undefined); + }); + + it('should handle equal wait and maxWait values and only invoke func once', () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100, { maxWait: 100 }); + + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + + expect(callback).toHaveBeenCalledTimes(1); + + const retval = debouncedCallback(); + expect(retval).toBe('foo'); + + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + + expect(callback).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 3c6b41de60f5..6430f0f23da5 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -14,6 +14,7 @@ import type { import { captureEvent, convertSpanLinksForEnvelope, + debounce, getCapturedScopesOnSpan, getDynamicSamplingContextFromSpan, getStatusMessage, @@ -49,8 +50,6 @@ interface FinishedSpanBucket { * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. */ export class SentrySpanExporter { - private _flushTimeout: ReturnType | undefined; - /* * A quick explanation on the buckets: We do bucketing of finished spans for efficiency. This span exporter is * accumulating spans until a root span is encountered and then it flushes all the spans that are descendants of that @@ -74,6 +73,8 @@ export class SentrySpanExporter { // Essentially a a set of span ids that are already sent. The values are expiration // times in this cache so we don't hold onto them indefinitely. private _sentSpans: Map; + /* Internally, we use a debounced flush to give some wiggle room to the span processor to accumulate more spans. */ + private _debouncedFlush: ReturnType; public constructor(options?: { /** Lower bound of time in seconds until spans that are buffered but have not been sent as part of a transaction get cleared from memory. */ @@ -84,50 +85,13 @@ export class SentrySpanExporter { this._lastCleanupTimestampInS = Math.floor(Date.now() / 1000); this._spansToBucketEntry = new WeakMap(); this._sentSpans = new Map(); + this._debouncedFlush = debounce(this.flush.bind(this), 1, { maxWait: 100 }); } /** - * Check if a span with the given ID has already been sent using the `_sentSpans` as a cache. - * Purges "expired" spans from the cache upon checking. - * @param spanId The span id to check. - * @returns Whether the span is already sent in the past X seconds. + * Export a single span. + * This is called by the span processor whenever a span is ended. */ - public isSpanAlreadySent(spanId: string): boolean { - const expirationTime = this._sentSpans.get(spanId); - if (expirationTime) { - if (Date.now() >= expirationTime) { - this._sentSpans.delete(spanId); // Remove expired span - } else { - return true; - } - } - return false; - } - - /** Remove "expired" span id entries from the _sentSpans cache. */ - public flushSentSpanCache(): void { - const currentTimestamp = Date.now(); - // Note, it is safe to delete items from the map as we go: https://stackoverflow.com/a/35943995/90297 - for (const [spanId, expirationTime] of this._sentSpans.entries()) { - if (expirationTime <= currentTimestamp) { - this._sentSpans.delete(spanId); - } - } - } - - /** Check if a node is a completed root node or a node whose parent has already been sent */ - public nodeIsCompletedRootNode(node: SpanNode): node is SpanNodeCompleted { - return !!node.span && (!node.parentNode || this.isSpanAlreadySent(node.parentNode.id)); - } - - /** Get all completed root nodes from a list of nodes */ - public getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { - // TODO: We should be able to remove the explicit `node is SpanNodeCompleted` type guard - // once we stop supporting TS < 5.5 - return nodes.filter((node): node is SpanNodeCompleted => this.nodeIsCompletedRootNode(node)); - } - - /** Export a single span. */ public export(span: ReadableSpan): void { const currentTimestampInS = Math.floor(Date.now() / 1000); @@ -159,26 +123,20 @@ export class SentrySpanExporter { // If the span doesn't have a local parent ID (it's a root span), we're gonna flush all the ended spans const localParentId = getLocalParentId(span); - if (!localParentId || this.isSpanAlreadySent(localParentId)) { - this._clearTimeout(); - - // If we got a parent span, we try to send the span tree - // Wait a tick for this, to ensure we avoid race conditions - this._flushTimeout = setTimeout(() => { - this.flush(); - }, 1); + if (!localParentId || this._sentSpans.has(localParentId)) { + this._debouncedFlush(); } } - /** Try to flush any pending spans immediately. */ + /** + * Try to flush any pending spans immediately. + * This is called internally by the exporter (via _debouncedFlush), + * but can also be triggered externally if we force-flush. + */ public flush(): void { - this._clearTimeout(); + const finishedSpans = this._finishedSpanBuckets.flatMap(bucket => (bucket ? Array.from(bucket.spans) : [])); - const finishedSpans: ReadableSpan[] = this._finishedSpanBuckets.flatMap(bucket => - bucket ? Array.from(bucket.spans) : [], - ); - - this.flushSentSpanCache(); + this._flushSentSpanCache(); const sentSpans = this._maybeSend(finishedSpans); const sentSpanCount = sentSpans.size; @@ -197,20 +155,20 @@ export class SentrySpanExporter { bucketEntry.spans.delete(span); } } + // Cancel a pending debounced flush, if there is one + // This can be relevant if we directly flush, circumventing the debounce + // in that case, we want to cancel any pending debounced flush + this._debouncedFlush.cancel(); } - /** Clear the exporter. */ + /** + * Clear the exporter. + * This is called when the span processor is shut down. + */ public clear(): void { this._finishedSpanBuckets = this._finishedSpanBuckets.fill(undefined); - this._clearTimeout(); - } - - /** Clear the flush timeout. */ - private _clearTimeout(): void { - if (this._flushTimeout) { - clearTimeout(this._flushTimeout); - this._flushTimeout = undefined; - } + this._sentSpans.clear(); + this._debouncedFlush.cancel(); } /** @@ -226,7 +184,7 @@ export class SentrySpanExporter { const grouped = groupSpansWithParents(spans); const sentSpans = new Set(); - const rootNodes = this.getCompletedRootNodes(grouped); + const rootNodes = this._getCompletedRootNodes(grouped); for (const root of rootNodes) { const span = root.span; @@ -257,6 +215,29 @@ export class SentrySpanExporter { return sentSpans; } + + /** Remove "expired" span id entries from the _sentSpans cache. */ + private _flushSentSpanCache(): void { + const currentTimestamp = Date.now(); + // Note, it is safe to delete items from the map as we go: https://stackoverflow.com/a/35943995/90297 + for (const [spanId, expirationTime] of this._sentSpans.entries()) { + if (expirationTime <= currentTimestamp) { + this._sentSpans.delete(spanId); + } + } + } + + /** Check if a node is a completed root node or a node whose parent has already been sent */ + private _nodeIsCompletedRootNodeOrHasSentParent(node: SpanNode): node is SpanNodeCompleted { + return !!node.span && (!node.parentNode || this._sentSpans.has(node.parentNode.id)); + } + + /** Get all completed root nodes from a list of nodes */ + private _getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { + // TODO: We should be able to remove the explicit `node is SpanNodeCompleted` type guard + // once we stop supporting TS < 5.5 + return nodes.filter((node): node is SpanNodeCompleted => this._nodeIsCompletedRootNodeOrHasSentParent(node)); + } } function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { diff --git a/packages/replay-internal/src/util/debounce.ts b/packages/replay-internal/src/util/debounce.ts index 8948b937febd..ea357ff1885c 100644 --- a/packages/replay-internal/src/util/debounce.ts +++ b/packages/replay-internal/src/util/debounce.ts @@ -1,3 +1,4 @@ +import { debounce as debounceCore } from '@sentry/core'; import { setTimeout } from '@sentry-internal/browser-utils'; type DebouncedCallback = { @@ -27,46 +28,9 @@ type DebounceOptions = { maxWait?: number }; * - `cancel`: Cancels the debouncing process and resets the debouncing timer */ export function debounce(func: CallbackFunction, wait: number, options?: DebounceOptions): DebouncedCallback { - let callbackReturnValue: unknown; - - let timerId: ReturnType | undefined; - let maxTimerId: ReturnType | undefined; - - const maxWait = options?.maxWait ? Math.max(options.maxWait, wait) : 0; - - function invokeFunc(): unknown { - cancelTimers(); - callbackReturnValue = func(); - return callbackReturnValue; - } - - function cancelTimers(): void { - timerId !== undefined && clearTimeout(timerId); - maxTimerId !== undefined && clearTimeout(maxTimerId); - timerId = maxTimerId = undefined; - } - - function flush(): unknown { - if (timerId !== undefined || maxTimerId !== undefined) { - return invokeFunc(); - } - return callbackReturnValue; - } - - function debounced(): unknown { - if (timerId) { - clearTimeout(timerId); - } - timerId = setTimeout(invokeFunc, wait); - - if (maxWait && maxTimerId === undefined) { - maxTimerId = setTimeout(invokeFunc, maxWait); - } - - return callbackReturnValue; - } - - debounced.cancel = cancelTimers; - debounced.flush = flush; - return debounced; + return debounceCore(func, wait, { + ...options, + // @ts-expect-error - Not quite sure why these types do not match, but this is fine + setTimeoutImpl: setTimeout, + }); } From 23127b6d1b151e1c7220fb4405842d34153a4728 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Tue, 17 Jun 2025 13:09:02 +0200 Subject: [PATCH 04/25] chore(browser): Update comment for fetch transport (#16617) Replaces https://github.com/getsentry/sentry-javascript/pull/16532 Updates the comment in the browser fetch transport to explain why we do not need to suppress tracing there. --- packages/browser/src/transports/fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 05ec0f2e211a..d21ae82486ec 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -45,7 +45,7 @@ export function makeFetchTransport( } try { - // TODO: This may need a `suppressTracing` call in the future when we switch the browser SDK to OTEL + // Note: We do not need to suppress tracing here, becasue we are using the native fetch, instead of our wrapped one. return nativeFetch(options.url, requestOptions).then(response => { pendingBodySize -= requestSize; pendingCount--; From 98ee4cc55b08a2299af67231e7b5462a17cac22e Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:40:56 -0700 Subject: [PATCH 05/25] feat(flags): capture feature flag evaluations on spans (#16485) Based off https://github.com/getsentry/sentry-python/pull/4280/files Reopened from https://github.com/getsentry/sentry-javascript/pull/16475 This PR updates our 5 browser FF integrations to capture flag evaluations on spans. This implementation avoids changes to the `Span` class, saving bundle size. - on eval, flags are added to span attributes. A global WeakMap is used to track the unique flags for a span. Updates to a flag's value is allowed. - staying consistent with the python PR: - we only capture values for the **first 10 unique flags** per span. Subsequent flags are dropped. - attribute keys have the format `flag.evaluation.{flagKey}` From @AbhiPrasad : > A method on the span break our compat with OTEL spans, which we use for duck typing in some scenarios. From @cmanallen : > For spans the oldest flag evaluations are the most important because they're likely to have the largest performance impact. We drop late arrivals due to request size concerns. --- .../integrations/featureFlags/constants.ts | 4 +- .../featureFlags/{ => onError}/basic/test.ts | 10 ++- .../featureFlags/{ => onError}/init.js | 0 .../featureFlags/{ => onError}/subject.js | 0 .../featureFlags/{ => onError}/template.html | 0 .../{ => onError}/withScope/test.ts | 8 ++- .../featureFlags/featureFlags/onSpan/init.js | 13 ++++ .../featureFlags/onSpan/subject.js | 16 +++++ .../featureFlags/onSpan/template.html | 12 ++++ .../featureFlags/featureFlags/onSpan/test.ts | 66 +++++++++++++++++ .../launchdarkly/{ => onError}/basic/test.ts | 10 ++- .../launchdarkly/{ => onError}/init.js | 0 .../launchdarkly/{ => onError}/subject.js | 0 .../launchdarkly/{ => onError}/template.html | 0 .../{ => onError}/withScope/test.ts | 8 ++- .../featureFlags/launchdarkly/onSpan/init.js | 39 ++++++++++ .../launchdarkly/onSpan/subject.js | 16 +++++ .../launchdarkly/onSpan/template.html | 12 ++++ .../featureFlags/launchdarkly/onSpan/test.ts | 66 +++++++++++++++++ .../openfeature/{ => onError}/basic/test.ts | 10 ++- .../{ => onError}/errorHook/init.js | 0 .../{ => onError}/errorHook/test.ts | 10 ++- .../openfeature/{ => onError}/init.js | 0 .../openfeature/{ => onError}/subject.js | 0 .../openfeature/{ => onError}/template.html | 0 .../{ => onError}/withScope/test.ts | 8 ++- .../featureFlags/openfeature/onSpan/init.js | 24 +++++++ .../openfeature/onSpan/subject.js | 16 +++++ .../openfeature/onSpan/template.html | 12 ++++ .../featureFlags/openfeature/onSpan/test.ts | 66 +++++++++++++++++ .../statsig/{ => onError}/basic/test.ts | 10 ++- .../statsig/{ => onError}/init.js | 0 .../statsig/{ => onError}/subject.js | 0 .../statsig/{ => onError}/template.html | 0 .../statsig/{ => onError}/withScope/test.ts | 8 ++- .../featureFlags/statsig/onSpan/init.js | 39 ++++++++++ .../featureFlags/statsig/onSpan/subject.js | 16 +++++ .../featureFlags/statsig/onSpan/template.html | 12 ++++ .../featureFlags/statsig/onSpan/test.ts | 70 ++++++++++++++++++ .../unleash/{ => onError}/basic/test.ts | 10 ++- .../unleash/{ => onError}/init.js | 0 .../unleash/{ => onError}/subject.js | 0 .../unleash/{ => onError}/template.html | 0 .../unleash/{ => onError}/withScope/test.ts | 8 ++- .../featureFlags/unleash/onSpan/init.js | 59 +++++++++++++++ .../featureFlags/unleash/onSpan/subject.js | 16 +++++ .../featureFlags/unleash/onSpan/template.html | 12 ++++ .../featureFlags/unleash/onSpan/test.ts | 68 ++++++++++++++++++ .../featureFlags/featureFlagsIntegration.ts | 8 +-- .../featureFlags/launchdarkly/integration.ts | 13 ++-- .../featureFlags/openfeature/integration.ts | 20 ++++-- .../featureFlags/statsig/integration.ts | 13 ++-- .../featureFlags/unleash/integration.ts | 11 +-- packages/browser/src/utils/featureFlags.ts | 72 +++++++++++++++---- packages/core/src/utils-hoist/worldwide.ts | 5 ++ 55 files changed, 829 insertions(+), 67 deletions(-) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/basic/test.ts (86%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/withScope/test.ts (91%) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/basic/test.ts (85%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/withScope/test.ts (90%) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/basic/test.ts (85%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/errorHook/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/errorHook/test.ts (85%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/withScope/test.ts (90%) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/basic/test.ts (85%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/withScope/test.ts (91%) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/basic/test.ts (88%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/withScope/test.ts (90%) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts index 680105d242e5..ba3c35a08241 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts @@ -1 +1,3 @@ -export const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. +// Corresponds to constants in featureFlags.ts, in browser utils. +export const FLAG_BUFFER_SIZE = 100; +export const MAX_FLAGS_PER_SPAN = 10; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts similarity index 86% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts index b63583906cc4..742cdd42109b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts similarity index 91% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts index 41418122b526..fecc762d4c99 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js new file mode 100644 index 000000000000..fa6a67ec3711 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + Sentry.featureFlagsIntegration(), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts new file mode 100644 index 000000000000..476b76d03475 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); + for (let i = 1; i <= maxFlags; i++) { + flagsIntegration.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration.addFeatureFlag(`feat${maxFlags + 1}`, true); // dropped flag + flagsIntegration.addFeatureFlag('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts similarity index 85% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts index 5d7f58bdb27b..1c1a04595187 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts index 78703e4e5389..2efb3fdc9ad0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js new file mode 100644 index 000000000000..9e4b802f28f3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryLDIntegration = Sentry.launchDarklyIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + window.sentryLDIntegration, + ], +}); + +// Manually mocking this because LD only has mock test utils for the React SDK. +// Also, no SDK has mock utils for FlagUsedHandler's. +const MockLaunchDarkly = { + initialize(_clientId, context, options) { + const flagUsedHandler = options.inspectors ? options.inspectors[0].method : undefined; + + return { + variation(key, defaultValue) { + if (flagUsedHandler) { + flagUsedHandler(key, { value: defaultValue }, context); + } + return defaultValue; + }, + }; + }, +}; + +window.initializeLD = () => { + return MockLaunchDarkly.initialize( + 'example-client-id', + { kind: 'user', key: 'example-context-key' }, + { inspectors: [Sentry.buildLaunchDarklyFlagUsedHandler()] }, + ); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts new file mode 100644 index 000000000000..965f00f91fa0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const ldClient = (window as any).initializeLD(); + for (let i = 1; i <= maxFlags; i++) { + ldClient.variation(`feat${i}`, false); + } + ldClient.variation(`feat${maxFlags + 1}`, true); // dropped + ldClient.variation('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts similarity index 85% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts index 77112ee82658..84deca47415d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts similarity index 85% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index d8f1e1311dfa..c2de7f54abd7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts index 67e68becb104..14cc072af30d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js new file mode 100644 index 000000000000..4fc1cace150c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryOpenFeatureIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); + +window.initialize = () => { + return { + getBooleanValue(flag, value) { + let hook = new Sentry.OpenFeatureIntegrationHook(); + hook.after(null, { flagKey: flag, value: value }); + return value; + }, + }; +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts new file mode 100644 index 000000000000..f3b43425477f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = (window as any).initialize(); + for (let i = 1; i <= maxFlags; i++) { + client.getBooleanValue(`feat${i}`, false); + } + client.getBooleanValue(`feat${maxFlags + 1}`, true); // drop + client.getBooleanValue('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts similarity index 85% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts index cb434e49e86e..331dbb8ad433 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts similarity index 91% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts index 42ee35e4604d..e80c6dbfc5fa 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js new file mode 100644 index 000000000000..22f74d2ebd7c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +class MockStatsigClient { + constructor() { + this._gateEvaluationListeners = []; + this._mockGateValues = {}; + } + + on(event, listener) { + this._gateEvaluationListeners.push(listener); + } + + checkGate(name) { + const value = this._mockGateValues[name] || false; // unknown features default to false. + this._gateEvaluationListeners.forEach(listener => { + listener({ gate: { name, value } }); + }); + return value; + } + + setMockGateValue(name, value) { + this._mockGateValues[name] = value; + } +} + +window.statsigClient = new MockStatsigClient(); + +window.Sentry = Sentry; +window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryStatsigIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts new file mode 100644 index 000000000000..dec534f9ffab --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -0,0 +1,70 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = (window as any).statsigClient; + for (let i = 1; i <= maxFlags; i++) { + client.checkGate(`feat${i}`); // values default to false + } + + client.setMockGateValue(`feat${maxFlags + 1}`, true); + client.checkGate(`feat${maxFlags + 1}`); // dropped + + client.setMockGateValue('feat3', true); + client.checkGate('feat3'); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts similarity index 88% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts index b2e522fc78f4..341bbbd03e96 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts index a512882b568a..fe3aec3ff188 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js new file mode 100644 index 000000000000..93993d8f6188 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js @@ -0,0 +1,59 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + constructor() { + this._featureToVariant = { + strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } }, + noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true }, + jsonFeat: { + name: 'paid-orgs', + enabled: true, + feature_enabled: true, + payload: { + type: 'json', + value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}', + }, + }, + + // Enabled feature with no configured variants. + noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true }, + + // Disabled feature. + disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false }, + }; + + // Variant returned for features that don't exist. + // `feature_enabled` may be defined in prod, but we want to test the undefined case. + this._fallbackVariant = { + name: 'disabled', + enabled: false, + }; + } + + isEnabled(toggleName) { + const variant = this._featureToVariant[toggleName] || this._fallbackVariant; + return variant.feature_enabled || false; + } + + getVariant(toggleName) { + return this._featureToVariant[toggleName] || this._fallbackVariant; + } +}; + +// Not a mock UnleashClient class method since it needs to match the signature of the actual UnleashClient. +window.setVariant = (client, featureName, variantName, isEnabled) => { + client._featureToVariant[featureName] = { name: variantName, enabled: isEnabled, feature_enabled: isEnabled }; +}; + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryUnleashIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts new file mode 100644 index 000000000000..b2607ffa4c07 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -0,0 +1,68 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = new (window as any).UnleashClient(); + for (let i = 1; i <= maxFlags; i++) { + client.isEnabled(`feat${i}`); + } + client.isEnabled(`feat${maxFlags + 1}`); // dropped + + (window as any).setVariant(client, 'feat3', 'var1', true); + client.isEnabled('feat3'); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts index 54b5680cccd1..e11084c84c2d 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,15 +1,14 @@ import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { addFeatureFlag: (name: string, value: unknown) => void; } /** - * Sentry integration for buffering feature flags manually with an API, and - * capturing them on error events. We recommend you do this on each flag - * evaluation. Flags are buffered per Sentry scope and limited to 100 per event. + * Sentry integration for buffering feature flag evaluations manually with an API, and + * capturing them on error events and spans. * * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. * @@ -41,6 +40,7 @@ export const featureFlagsIntegration = defineIntegration(() => { addFeatureFlag(name: string, value: unknown): void { insertFlagToScope(name, value); + addFeatureFlagToActiveSpan(name, value); }, }; }) as IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index f96b8deb8fa0..eeb20dc07cf9 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,10 +1,10 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** - * Sentry integration for capturing feature flags from LaunchDarkly. + * Sentry integration for capturing feature flag evaluations from LaunchDarkly. * * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. * @@ -29,10 +29,10 @@ export const launchDarklyIntegration = defineIntegration(() => { }) satisfies IntegrationFn; /** - * LaunchDarkly hook that listens for flag evaluations and updates the `flags` - * context in our Sentry scope. This needs to be registered as an - * 'inspector' in LaunchDarkly initialize() options, separately from - * `launchDarklyIntegration`. Both are needed to collect feature flags on error. + * LaunchDarkly hook to listen for and buffer flag evaluations. This needs to + * be registered as an 'inspector' in LaunchDarkly initialize() options, + * separately from `launchDarklyIntegration`. Both the hook and the integration + * are needed to capture LaunchDarkly flags. */ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler { return { @@ -46,6 +46,7 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { insertFlagToScope(flagKey, flagDetail.value); + addFeatureFlagToActiveSpan(flagKey, flagDetail.value); }, }; } diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index b1963e9964e6..79dc97394cce 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -1,13 +1,21 @@ /** - * OpenFeature integration. + * Sentry integration for capturing OpenFeature feature flag evaluations. * - * Add the openFeatureIntegration() function call to your integration lists. - * Add the integration hook to your OpenFeature object. - * - OpenFeature.getClient().addHooks(new OpenFeatureIntegrationHook()); + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import * as Sentry from "@sentry/browser"; + * import { OpenFeature } from "@openfeature/web-sdk"; + * + * Sentry.init(..., integrations: [Sentry.openFeatureIntegration()]); + * OpenFeature.setProvider(new MyProviderOfChoice()); + * OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook()); + * ``` */ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { @@ -29,6 +37,7 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + addFeatureFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); } /** @@ -36,5 +45,6 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + addFeatureFlagToActiveSpan(hookContext.flagKey, hookContext.defaultValue); } } diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index 54600458cfb9..ee472d77669a 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -31,15 +31,16 @@ export const statsigIntegration = defineIntegration( return { name: 'Statsig', - processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); - }, - - setup() { + setup(_client: Client) { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { insertFlagToScope(event.gate.name, event.gate.value); + addFeatureFlagToActiveSpan(event.gate.name, event.gate.value); }); }, + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, }; }, ) satisfies IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index 21d945dfcaae..ee7e2a3a0d4d 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,7 +1,7 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration, fill, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { @@ -35,14 +35,14 @@ export const unleashIntegration = defineIntegration( return { name: 'Unleash', - processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); - }, - setupOnce() { const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); }, + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, }; }, ) satisfies IntegrationFn; @@ -65,6 +65,7 @@ function _wrappedIsEnabled( if (typeof toggleName === 'string' && typeof result === 'boolean') { insertFlagToScope(toggleName, result); + addFeatureFlagToActiveSpan(toggleName, result); } else if (DEBUG_BUILD) { logger.error( `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index a71e7233fe75..9ae389773bcd 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -1,5 +1,5 @@ -import type { Event, FeatureFlag } from '@sentry/core'; -import { getCurrentScope, logger } from '@sentry/core'; +import type { Event, FeatureFlag, Span } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, GLOBAL_OBJ, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; /** @@ -13,6 +13,16 @@ import { DEBUG_BUILD } from '../debug-build'; */ export const FLAG_BUFFER_SIZE = 100; +/** + * Max number of flag evaluations to record per span. + */ +export const MAX_FLAGS_PER_SPAN = 10; + +// Global map of spans to feature flag buffers. Populated by feature flag integrations. +GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap>(); + +const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.'; + /** * Copies feature flags that are in current scope context to the event context */ @@ -33,18 +43,15 @@ export function copyFlagsFromScopeToEvent(event: Event): Event { } /** - * Creates a feature flags values array in current context if it does not exist - * and inserts the flag into a FeatureFlag array while maintaining ordered LRU - * properties. Not thread-safe. After inserting: - * - `flags` is sorted in order of recency, with the newest flag at the end. - * - No other flags with the same name exist in `flags`. - * - The length of `flags` does not exceed `maxSize`. The oldest flag is evicted - * as needed. + * Inserts a flag into the current scope's context while maintaining ordered LRU properties. + * Not thread-safe. After inserting: + * - The flag buffer is sorted in order of recency, with the newest evaluation at the end. + * - The names in the buffer are always unique. + * - The length of the buffer never exceeds `maxSize`. * * @param name Name of the feature flag to insert. * @param value Value of the feature flag. - * @param maxSize Max number of flags the buffer should store. It's recommended - * to keep this consistent across insertions. Default is FLAG_BUFFER_SIZE + * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. */ export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { const scopeContexts = getCurrentScope().getScopeData().contexts; @@ -56,7 +63,16 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number } /** - * Exported for tests. Currently only accepts boolean values (otherwise no-op). + * Exported for tests only. Currently only accepts boolean values (otherwise no-op). + * Inserts a flag into a FeatureFlag array while maintaining the following properties: + * - Flags are sorted in order of recency, with the newest evaluation at the end. + * - The flag names are always unique. + * - The length of the array never exceeds `maxSize`. + * + * @param flags The buffer to insert the flag into. + * @param name Name of the feature flag to insert. + * @param value Value of the feature flag. + * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. */ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number): void { if (typeof value !== 'boolean') { @@ -87,3 +103,35 @@ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: un result: value, }); } + +/** + * Records a feature flag evaluation for the active span. This is a no-op for non-boolean values. + * The flag and its value is stored in span attributes with the `flag.evaluation` prefix. Once the + * unique flags for a span reaches maxFlagsPerSpan, subsequent flags are dropped. + * + * @param name Name of the feature flag. + * @param value Value of the feature flag. Non-boolean values are ignored. + * @param maxFlagsPerSpan Max number of flags a buffer should store. Default value should always be used in production. + */ +export function addFeatureFlagToActiveSpan( + name: string, + value: unknown, + maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, +): void { + const spanFlagMap = GLOBAL_OBJ._spanToFlagBufferMap; + if (!spanFlagMap || typeof value !== 'boolean') { + return; + } + + const span = getActiveSpan(); + if (span) { + const flags = spanFlagMap.get(span) || new Set(); + if (flags.has(name)) { + span.setAttribute(`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } else if (flags.size < maxFlagsPerSpan) { + flags.add(name); + span.setAttribute(`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } + spanFlagMap.set(span, flags); + } +} diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 3a396d96f809..70196e4b0c8b 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -15,6 +15,7 @@ import type { Carrier } from '../carrier'; import type { Client } from '../client'; import type { SerializedLog } from '../types-hoist/log'; +import type { Span } from '../types-hoist/span'; import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ @@ -56,6 +57,10 @@ export type InternalGlobal = { */ _sentryModuleMetadata?: Record; _sentryEsmLoaderHookRegistered?: boolean; + /** + * A map of spans to evaluated feature flags. Populated by feature flag integrations. + */ + _spanToFlagBufferMap?: WeakMap>; } & Carrier; /** Get's the global object for the current JavaScript runtime */ From 468fc8ca5509b8fe6c15f3288240c0a9318b834d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:13:20 -0700 Subject: [PATCH 06/25] feat(flags): add node support for generic featureFlagsIntegration and move utils to core (#16585) The `featureFlagsIntegration` is an integration to manually buffer feature flags on evaluation, and capture them in event contexts and span attributes. This PR moves it from browser to core, as well as the shared functionality/utils of all FF integrations (no browser specific logic). Browser exports and functionality is unchanged. Per @AbhiPrasad 's recommendation I've manually exported the integration in all the packages `zodErrorsIntegration` is exported. Note many backend pkgs use a wildcard (*) export from node. TODO: - [x] add node-integration-tests - [ ] update platform docs Part of - https://github.com/getsentry/team-replay/issues/510 --- .../integrations/featureFlags/constants.ts | 3 -- .../featureFlags/onError/basic/test.ts | 2 +- .../featureFlags/featureFlags/onSpan/test.ts | 2 +- .../launchdarkly/onError/basic/test.ts | 2 +- .../featureFlags/launchdarkly/onSpan/test.ts | 2 +- .../openfeature/onError/basic/test.ts | 2 +- .../openfeature/onError/errorHook/test.ts | 2 +- .../featureFlags/openfeature/onSpan/test.ts | 2 +- .../statsig/onError/basic/test.ts | 2 +- .../featureFlags/statsig/onSpan/test.ts | 2 +- .../unleash/onError/basic/test.ts | 2 +- .../featureFlags/unleash/onSpan/test.ts | 2 +- .../onError/basic/scenario.ts | 19 ++++++++ .../onError/basic/test.ts | 31 ++++++++++++ .../onError/withScope/scenario.ts | 22 +++++++++ .../onError/withScope/test.ts | 38 +++++++++++++++ .../onSpan/scenario.ts | 25 ++++++++++ .../featureFlagsIntegration/onSpan/test.ts | 33 +++++++++++++ packages/astro/src/index.server.ts | 2 + packages/aws-serverless/src/index.ts | 2 + packages/browser/src/index.ts | 4 +- .../featureFlags/launchdarkly/integration.ts | 14 ++++-- .../featureFlags/openfeature/integration.ts | 18 ++++--- .../featureFlags/statsig/integration.ts | 14 ++++-- .../featureFlags/unleash/integration.ts | 16 +++++-- packages/bun/src/index.ts | 2 + packages/cloudflare/src/index.ts | 2 + packages/core/src/featureFlags.ts | 1 - packages/core/src/index.ts | 10 +++- .../featureFlags/featureFlagsIntegration.ts | 18 ++++--- .../src/integrations/featureFlags/index.ts | 0 packages/core/src/types-hoist/context.ts | 2 +- .../src/utils/featureFlags.ts | 35 ++++++++++---- .../test/lib}/utils/featureFlags.test.ts | 48 ++++++++++--------- packages/deno/src/index.ts | 2 + packages/google-cloud-serverless/src/index.ts | 2 + packages/node/src/index.ts | 2 + packages/remix/src/cloudflare/index.ts | 2 + packages/sveltekit/src/worker/index.ts | 2 + packages/vercel-edge/src/index.ts | 2 + 40 files changed, 314 insertions(+), 79 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts delete mode 100644 packages/core/src/featureFlags.ts rename packages/{browser => core}/src/integrations/featureFlags/featureFlagsIntegration.ts (69%) rename packages/{browser => core}/src/integrations/featureFlags/index.ts (100%) rename packages/{browser => core}/src/utils/featureFlags.ts (80%) rename packages/{browser/test => core/test/lib}/utils/featureFlags.test.ts (62%) diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts deleted file mode 100644 index ba3c35a08241..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Corresponds to constants in featureFlags.ts, in browser utils. -export const FLAG_BUFFER_SIZE = 100; -export const MAX_FLAGS_PER_SPAN = 10; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts index 742cdd42109b..3233c9047649 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest, } from '../../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index 476b76d03475..6516d8e36abb 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest, } from '../../../../../utils/helpers'; -import { MAX_FLAGS_PER_SPAN } from '../../constants'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts index 1c1a04595187..bc3e0afdc292 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest, } from '../../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts index 965f00f91fa0..eb7eb003c838 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest, } from '../../../../../utils/helpers'; -import { MAX_FLAGS_PER_SPAN } from '../../constants'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts index 84deca47415d..5953f1e0b087 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest, } from '../../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index c2de7f54abd7..89654c82eda1 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest, } from '../../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts index f3b43425477f..5ade5d01b3d5 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest, } from '../../../../../utils/helpers'; -import { MAX_FLAGS_PER_SPAN } from '../../constants'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts index 331dbb8ad433..134b29417d53 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest, } from '../../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts index dec534f9ffab..1ea192f98850 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest, } from '../../../../../utils/helpers'; -import { MAX_FLAGS_PER_SPAN } from '../../constants'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts index 341bbbd03e96..6e2760b69600 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest, } from '../../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts index b2607ffa4c07..984bba3bc0e3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest, } from '../../../../../utils/helpers'; -import { MAX_FLAGS_PER_SPAN } from '../../constants'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts new file mode 100644 index 000000000000..6d1b88137b87 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts @@ -0,0 +1,19 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); +for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); +} +flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction +flagsIntegration?.addFeatureFlag('feat3', true); // update + +throw new Error('Test error'); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts new file mode 100644 index 000000000000..74ff1c125b45 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts @@ -0,0 +1,31 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error with eviction, update, and no async tasks', async () => { + // Based on scenario.ts. + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Test error' }] }, + contexts: { + flags: { + values: expectedFlags, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts new file mode 100644 index 000000000000..b6537011a87f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts @@ -0,0 +1,22 @@ +import type { Scope } from '@sentry/node'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); +flagsIntegration?.addFeatureFlag('shared', true); + +Sentry.withScope((_scope: Scope) => { + flagsIntegration?.addFeatureFlag('forked', true); + flagsIntegration?.addFeatureFlag('shared', false); + Sentry.captureException(new Error('Error in forked scope')); +}); + +flagsIntegration?.addFeatureFlag('main', true); +throw new Error('Error in main scope'); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts new file mode 100644 index 000000000000..947b299923e7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts @@ -0,0 +1,38 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error are isolated by current scope', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ], + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in main scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ], + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts new file mode 100644 index 000000000000..2c07e46b40ed --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -0,0 +1,25 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); + +Sentry.startSpan({ name: 'test-root-span' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'test-nested-span' }, () => { + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag + flagsIntegration?.addFeatureFlag('feat3', true); // update + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts new file mode 100644 index 000000000000..4a417a3c3959 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -0,0 +1,33 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on span attributes with max limit', async () => { + // Based on scenario.ts. + const expectedFlags: Record = {}; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedFlags[`flag.evaluation.feat${i}`] = i === 3; + } + + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + spans: [ + expect.objectContaining({ + description: 'test-span', + data: expect.objectContaining({}), + }), + expect.objectContaining({ + description: 'test-nested-span', + data: expect.objectContaining(expectedFlags), + }), + ], + }, + }) + .start() + .completed(); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index ac222eca825b..750eb05d8b10 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -135,6 +135,8 @@ export { consoleLoggingIntegration, wrapMcpServerWithSentry, NODE_VERSION, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 24513a325188..b13f69a9b6ce 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -121,6 +121,8 @@ export { consoleLoggingIntegration, wrapMcpServerWithSentry, NODE_VERSION, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 2b2279c099b3..963d8ab38546 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -61,13 +61,13 @@ export { instrumentSupabaseClient, zodErrorsIntegration, thirdPartyErrorFilterIntegration, + featureFlagsIntegration, } from '@sentry/core'; -export type { Span } from '@sentry/core'; +export type { Span, FeatureFlagsIntegration } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; export { spotlightBrowserIntegration } from './integrations/spotlight'; export { browserSessionIntegration } from './integrations/browsersession'; -export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; export { unleashIntegration } from './integrations/featureFlags/unleash'; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index eeb20dc07cf9..822e4b1d7f80 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,6 +1,10 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, +} from '@sentry/core'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** @@ -23,7 +27,7 @@ export const launchDarklyIntegration = defineIntegration(() => { name: 'LaunchDarkly', processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }) satisfies IntegrationFn; @@ -45,8 +49,8 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler * Handle a flag evaluation by storing its name and value on the current scope. */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { - insertFlagToScope(flagKey, flagDetail.value); - addFeatureFlagToActiveSpan(flagKey, flagDetail.value); + _INTERNAL_insertFlagToScope(flagKey, flagDetail.value); + _INTERNAL_addFeatureFlagToActiveSpan(flagKey, flagDetail.value); }, }; } diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index 79dc97394cce..85aedbf779f9 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -14,8 +14,12 @@ * ``` */ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, +} from '@sentry/core'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { @@ -23,7 +27,7 @@ export const openFeatureIntegration = defineIntegration(() => { name: 'OpenFeature', processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }) satisfies IntegrationFn; @@ -36,15 +40,15 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { * Successful evaluation result. */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { - insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); - addFeatureFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_addFeatureFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); } /** * On error evaluation result. */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { - insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); - addFeatureFlagToActiveSpan(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_addFeatureFlagToActiveSpan(hookContext.flagKey, hookContext.defaultValue); } } diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index ee472d77669a..9aef234045b5 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,6 +1,10 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, +} from '@sentry/core'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -33,13 +37,13 @@ export const statsigIntegration = defineIntegration( setup(_client: Client) { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { - insertFlagToScope(event.gate.name, event.gate.value); - addFeatureFlagToActiveSpan(event.gate.name, event.gate.value); + _INTERNAL_insertFlagToScope(event.gate.name, event.gate.value); + _INTERNAL_addFeatureFlagToActiveSpan(event.gate.name, event.gate.value); }); }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }, diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index ee7e2a3a0d4d..699c797edecf 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,7 +1,13 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration, fill, logger } from '@sentry/core'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, + fill, + logger, +} from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { @@ -41,7 +47,7 @@ export const unleashIntegration = defineIntegration( }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }, @@ -64,8 +70,8 @@ function _wrappedIsEnabled( const result = original.apply(this, args); if (typeof toggleName === 'string' && typeof result === 'boolean') { - insertFlagToScope(toggleName, result); - addFeatureFlagToActiveSpan(toggleName, result); + _INTERNAL_insertFlagToScope(toggleName, result); + _INTERNAL_addFeatureFlagToActiveSpan(toggleName, result); } else if (DEBUG_BUILD) { logger.error( `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 70104de6d7c3..14a44e2d38fc 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -15,6 +15,7 @@ export type { Stacktrace, Thread, User, + FeatureFlagsIntegration, } from '@sentry/core'; export { @@ -139,6 +140,7 @@ export { consoleLoggingIntegration, createSentryWinstonTransport, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 6754ffd04f7c..1a366aeb5dd0 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -91,6 +92,7 @@ export { updateSpanName, wrapMcpServerWithSentry, consoleLoggingIntegration, + featureFlagsIntegration, } from '@sentry/core'; export * as logger from './logs/exports'; diff --git a/packages/core/src/featureFlags.ts b/packages/core/src/featureFlags.ts deleted file mode 100644 index f80e17ef7f9d..000000000000 --- a/packages/core/src/featureFlags.ts +++ /dev/null @@ -1 +0,0 @@ -export type FeatureFlag = { readonly flag: string; readonly result: boolean }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d418b8a28c19..fdbcd8bffbef 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -113,6 +113,7 @@ export { supabaseIntegration, instrumentSupabaseClient } from './integrations/su export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; +export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { profiler } from './profiling'; export { instrumentFetchRequest } from './fetch'; @@ -123,7 +124,14 @@ export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; -export type { FeatureFlag } from './featureFlags'; +export type { FeatureFlag } from './utils/featureFlags'; +export { + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_FLAG_BUFFER_SIZE, + _INTERNAL_MAX_FLAGS_PER_SPAN, +} from './utils/featureFlags'; export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils-hoist/breadcrumb-log-level'; diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts similarity index 69% rename from packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts rename to packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts index e11084c84c2d..fddc1944ceab 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,6 +1,12 @@ -import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; +import { type Client } from '../../client'; +import { defineIntegration } from '../../integration'; +import { type Event, type EventHint } from '../../types-hoist/event'; +import { type Integration, type IntegrationFn } from '../../types-hoist/integration'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, +} from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { addFeatureFlag: (name: string, value: unknown) => void; @@ -35,12 +41,12 @@ export const featureFlagsIntegration = defineIntegration(() => { name: 'FeatureFlags', processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, addFeatureFlag(name: string, value: unknown): void { - insertFlagToScope(name, value); - addFeatureFlagToActiveSpan(name, value); + _INTERNAL_insertFlagToScope(name, value); + _INTERNAL_addFeatureFlagToActiveSpan(name, value); }, }; }) as IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/index.ts b/packages/core/src/integrations/featureFlags/index.ts similarity index 100% rename from packages/browser/src/integrations/featureFlags/index.ts rename to packages/core/src/integrations/featureFlags/index.ts diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts index 0ad6eebf6ac3..a3673f84a968 100644 --- a/packages/core/src/types-hoist/context.ts +++ b/packages/core/src/types-hoist/context.ts @@ -1,4 +1,4 @@ -import type { FeatureFlag } from '../featureFlags'; +import type { FeatureFlag } from '../utils/featureFlags'; import type { SpanLinkJSON } from './link'; import type { Primitive } from './misc'; import type { SpanOrigin } from './span'; diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts similarity index 80% rename from packages/browser/src/utils/featureFlags.ts rename to packages/core/src/utils/featureFlags.ts index 9ae389773bcd..388613394148 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -1,6 +1,10 @@ -import type { Event, FeatureFlag, Span } from '@sentry/core'; -import { getActiveSpan, getCurrentScope, GLOBAL_OBJ, logger } from '@sentry/core'; +import { getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; +import { type Event } from '../types-hoist/event'; +import { type Span } from '../types-hoist/span'; +import { logger } from '../utils-hoist/logger'; +import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { getActiveSpan } from './spanUtils'; /** * Ordered LRU cache for storing feature flags in the scope context. The name @@ -8,15 +12,17 @@ import { DEBUG_BUILD } from '../debug-build'; * from oldest to newest. */ +export type FeatureFlag = { readonly flag: string; readonly result: boolean }; + /** * Max size of the LRU flag buffer stored in Sentry scope and event contexts. */ -export const FLAG_BUFFER_SIZE = 100; +export const _INTERNAL_FLAG_BUFFER_SIZE = 100; /** * Max number of flag evaluations to record per span. */ -export const MAX_FLAGS_PER_SPAN = 10; +export const _INTERNAL_MAX_FLAGS_PER_SPAN = 10; // Global map of spans to feature flag buffers. Populated by feature flag integrations. GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap>(); @@ -26,7 +32,7 @@ const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.'; /** * Copies feature flags that are in current scope context to the event context */ -export function copyFlagsFromScopeToEvent(event: Event): Event { +export function _INTERNAL_copyFlagsFromScopeToEvent(event: Event): Event { const scope = getCurrentScope(); const flagContext = scope.getScopeData().contexts.flags; const flagBuffer = flagContext ? flagContext.values : []; @@ -53,13 +59,17 @@ export function copyFlagsFromScopeToEvent(event: Event): Event { * @param value Value of the feature flag. * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. */ -export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { +export function _INTERNAL_insertFlagToScope( + name: string, + value: unknown, + maxSize: number = _INTERNAL_FLAG_BUFFER_SIZE, +): void { const scopeContexts = getCurrentScope().getScopeData().contexts; if (!scopeContexts.flags) { scopeContexts.flags = { values: [] }; } const flags = scopeContexts.flags.values as FeatureFlag[]; - insertToFlagBuffer(flags, name, value, maxSize); + _INTERNAL_insertToFlagBuffer(flags, name, value, maxSize); } /** @@ -74,7 +84,12 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number * @param value Value of the feature flag. * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. */ -export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number): void { +export function _INTERNAL_insertToFlagBuffer( + flags: FeatureFlag[], + name: string, + value: unknown, + maxSize: number, +): void { if (typeof value !== 'boolean') { return; } @@ -113,10 +128,10 @@ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: un * @param value Value of the feature flag. Non-boolean values are ignored. * @param maxFlagsPerSpan Max number of flags a buffer should store. Default value should always be used in production. */ -export function addFeatureFlagToActiveSpan( +export function _INTERNAL_addFeatureFlagToActiveSpan( name: string, value: unknown, - maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, + maxFlagsPerSpan: number = _INTERNAL_MAX_FLAGS_PER_SPAN, ): void { const spanFlagMap = GLOBAL_OBJ._spanToFlagBufferMap; if (!spanFlagMap || typeof value !== 'boolean') { diff --git a/packages/browser/test/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts similarity index 62% rename from packages/browser/test/utils/featureFlags.test.ts rename to packages/core/test/lib/utils/featureFlags.test.ts index 1c0bed312590..14258c2caf36 100644 --- a/packages/browser/test/utils/featureFlags.test.ts +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -1,16 +1,20 @@ -import type { FeatureFlag } from '@sentry/core'; -import { getCurrentScope, logger } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { insertFlagToScope, insertToFlagBuffer } from '../../src/utils/featureFlags'; +import { getCurrentScope } from '../../../src/currentScopes'; +import { + type FeatureFlag, + _INTERNAL_insertFlagToScope, + _INTERNAL_insertToFlagBuffer, +} from '../../../src/utils/featureFlags'; +import { logger } from '../../../src/utils-hoist/logger'; describe('flags', () => { describe('insertFlagToScope()', () => { it('adds flags to the current scope context', () => { const maxSize = 3; - insertFlagToScope('feat1', true, maxSize); - insertFlagToScope('feat2', true, maxSize); - insertFlagToScope('feat3', true, maxSize); - insertFlagToScope('feat4', true, maxSize); + _INTERNAL_insertFlagToScope('feat1', true, maxSize); + _INTERNAL_insertFlagToScope('feat2', true, maxSize); + _INTERNAL_insertFlagToScope('feat3', true, maxSize); + _INTERNAL_insertFlagToScope('feat4', true, maxSize); const scope = getCurrentScope(); expect(scope.getScopeData().contexts.flags?.values).toEqual([ @@ -31,10 +35,10 @@ describe('flags', () => { it('maintains ordering and evicts the oldest entry', () => { const buffer: FeatureFlag[] = []; const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat4', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat4', true, maxSize); expect(buffer).toEqual([ { flag: 'feat2', result: true }, @@ -46,11 +50,11 @@ describe('flags', () => { it('does not duplicate same-name flags and updates order and values', () => { const buffer: FeatureFlag[] = []; const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', false, maxSize); - insertToFlagBuffer(buffer, 'feat1', false, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', false, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', false, maxSize); expect(buffer).toEqual([ { flag: 'feat2', result: true }, @@ -62,8 +66,8 @@ describe('flags', () => { it('does not allocate unnecessary space', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); expect(buffer).toEqual([ { flag: 'feat1', result: true }, @@ -74,8 +78,8 @@ describe('flags', () => { it('does not accept non-boolean values', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', 1, maxSize); - insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', 1, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); expect(buffer).toEqual([]); }); @@ -86,7 +90,7 @@ describe('flags', () => { { flag: 'feat2', result: true }, ]; - insertToFlagBuffer(buffer, 'feat1', true, 1); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, 1); expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), ); @@ -95,7 +99,7 @@ describe('flags', () => { { flag: 'feat2', result: true }, ]); - insertToFlagBuffer(buffer, 'feat1', true, -2); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, -2); expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), ); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f388de7cb5ee..12bcedc35270 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -87,6 +88,7 @@ export { spanToBaggageHeader, updateSpanName, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/core'; export { DenoClient } from './client'; diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index f2622e591497..e9586a9bd820 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -121,6 +121,8 @@ export { consoleLoggingIntegration, wrapMcpServerWithSentry, NODE_VERSION, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 589937b21fd4..cf951c3db8b6 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -139,6 +139,7 @@ export { consoleLoggingIntegration, consoleIntegration, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/core'; export type { @@ -158,6 +159,7 @@ export type { Thread, User, Span, + FeatureFlagsIntegration, } from '@sentry/core'; export { logger }; diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 3d3d17e1da27..46c443cac39f 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -32,6 +32,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -109,4 +110,5 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + featureFlagsIntegration, } from '@sentry/core'; diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 3614922072ec..2dde8c61a1dc 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -82,6 +82,8 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/cloudflare'; /** diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ff1231c8a1f8..303d40144ec3 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -90,6 +91,7 @@ export { spanToBaggageHeader, wrapMcpServerWithSentry, consoleLoggingIntegration, + featureFlagsIntegration, } from '@sentry/core'; export { VercelEdgeClient } from './client'; From 049d0d9102f52d88066552b91dc1c7f497676aab Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 18 Jun 2025 11:34:01 +0200 Subject: [PATCH 07/25] feat(core): Allow to pass `scope` & `client` to `getTraceData` (#16633) In addition to `span`, this allows to use this with custom client setups as well. Usage: ```js const traceData = Sentry.getTraceData({ scope, client }); ``` Closes https://github.com/getsentry/sentry-javascript/issues/16618 --- packages/core/src/utils/traceData.ts | 7 +++-- .../core/test/lib/utils/traceData.test.ts | 30 +++++++++++++++++++ packages/opentelemetry/src/propagator.ts | 12 +++++--- .../opentelemetry/src/utils/getTraceData.ts | 12 +++++--- .../test/utils/getTraceData.test.ts | 29 +++++++++++++++++- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index e9d284e5a2c5..ada865ed396d 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -1,5 +1,6 @@ import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; +import type { Client } from '../client'; import { getClient, getCurrentScope } from '../currentScopes'; import { isEnabled } from '../exports'; import type { Scope } from '../scope'; @@ -22,8 +23,8 @@ import { getActiveSpan, spanToTraceHeader } from './spanUtils'; * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header * or meta tag name. */ -export function getTraceData(options: { span?: Span } = {}): SerializedTraceData { - const client = getClient(); +export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { + const client = options.client || getClient(); if (!isEnabled() || !client) { return {}; } @@ -34,7 +35,7 @@ export function getTraceData(options: { span?: Span } = {}): SerializedTraceData return acs.getTraceData(options); } - const scope = getCurrentScope(); + const scope = options.scope || getCurrentScope(); const span = options.span || getActiveSpan(); const sentryTrace = span ? spanToTraceHeader(span) : scopeToTraceHeader(scope); const dsc = span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromScope(client, scope); diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index 8d38bdb062d4..d10bf1e3b592 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -6,6 +6,7 @@ import { getIsolationScope, getMainCarrier, getTraceData, + Scope, SentrySpan, setAsyncContextStrategy, setCurrentClient, @@ -158,6 +159,35 @@ describe('getTraceData', () => { }); }); + it('allows to pass a scope & client directly', () => { + // this default client & scope should not be used! + setupClient(); + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampleRand: 0.44, + }); + + const options = getDefaultTestClientOptions({ + dsn: 'https://567@sentry.io/42', + tracesSampleRate: 1, + }); + const customClient = new TestClient(options); + + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + scope.setClient(customClient); + + const traceData = getTraceData({ client: customClient, scope }); + + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=production,sentry-public_key=567,sentry-trace_id=12345678901234567890123456789012', + ); + }); + it('returns propagationContext DSC data if no span is available', () => { setupClient(); diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index b5e493b31fd8..30429a490b14 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -2,7 +2,7 @@ import type { Baggage, Context, Span, SpanContext, TextMapGetter, TextMapSetter import { context, INVALID_TRACEID, propagation, trace, TraceFlags } from '@opentelemetry/api'; import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; -import type { continueTrace, DynamicSamplingContext, Options } from '@sentry/core'; +import type { Client, continueTrace, DynamicSamplingContext, Options, Scope } from '@sentry/core'; import { generateSentryTraceHeader, getClient, @@ -152,8 +152,12 @@ export function shouldPropagateTraceForUrl( /** * Get propagation injection data for the given context. + * The additional options can be passed to override the scope and client that is otherwise derived from the context. */ -export function getInjectionData(context: Context): { +export function getInjectionData( + context: Context, + options: { scope?: Scope; client?: Client } = {}, +): { dynamicSamplingContext: Partial | undefined; traceId: string | undefined; spanId: string | undefined; @@ -190,8 +194,8 @@ export function getInjectionData(context: Context): { // Else we try to use the propagation context from the scope // The only scenario where this should happen is when we neither have a span, nor an incoming trace - const scope = getScopesFromContext(context)?.scope || getCurrentScope(); - const client = getClient(); + const scope = options.scope || getScopesFromContext(context)?.scope || getCurrentScope(); + const client = options.client || getClient(); const propagationContext = scope.getPropagationContext(); const dynamicSamplingContext = client ? getDynamicSamplingContextFromScope(client, scope) : undefined; diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 88ab5a349cdf..3c41c4f949de 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -1,5 +1,5 @@ import * as api from '@opentelemetry/api'; -import type { SerializedTraceData, Span } from '@sentry/core'; +import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, @@ -12,8 +12,12 @@ import { getContextFromScope } from './contextData'; * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceData { - let ctx = api.context.active(); +export function getTraceData({ + span, + scope, + client, +}: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { + let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { const { scope } = getCapturedScopesOnSpan(span); @@ -21,7 +25,7 @@ export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceDat ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); } - const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx); + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); return { 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), diff --git a/packages/opentelemetry/test/utils/getTraceData.test.ts b/packages/opentelemetry/test/utils/getTraceData.test.ts index a4fc5919e1b6..d94e86930619 100644 --- a/packages/opentelemetry/test/utils/getTraceData.test.ts +++ b/packages/opentelemetry/test/utils/getTraceData.test.ts @@ -1,9 +1,10 @@ import { context, trace } from '@opentelemetry/api'; -import { getCurrentScope, setAsyncContextStrategy } from '@sentry/core'; +import { getCurrentScope, Scope, setAsyncContextStrategy } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getTraceData } from '../../src/utils/getTraceData'; import { makeTraceState } from '../../src/utils/makeTraceState'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('getTraceData', () => { beforeEach(() => { @@ -52,6 +53,32 @@ describe('getTraceData', () => { }); }); + it('allows to pass a scope & client directly', () => { + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampleRand: 0.44, + }); + + const customClient = new TestClient( + getDefaultTestClientOptions({ tracesSampleRate: 1, dsn: 'https://123@sentry.io/42' }), + ); + + // note: Right now, this only works properly if the scope is linked to a context + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + scope.setClient(customClient); + + const traceData = getTraceData({ client: customClient, scope }); + + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012', + ); + }); + it('returns propagationContext DSC data if no span is available', () => { getCurrentScope().setPropagationContext({ traceId: '12345678901234567890123456789012', From 784869decaa1f08d834968c7a57c00590b024ab5 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 18 Jun 2025 11:43:25 +0200 Subject: [PATCH 08/25] fix(nextjs): Fix lookup of `instrumentation-client.js` file (#16637) Fixes https://github.com/getsentry/sentry-javascript/issues/16634 --- packages/nextjs/src/config/withSentryConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index b94ce6187f97..b185cc3d3211 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -432,7 +432,7 @@ function getInstrumentationClientFileContents(): string | void { ['src', 'instrumentation-client.ts'], ['src', 'instrumentation-client.js'], ['instrumentation-client.ts'], - ['instrumentation-client.ts'], + ['instrumentation-client.js'], ]; for (const pathSegments of potentialInstrumentationClientFileLocations) { From 8f9b9dffa3d73247f2ad9cce65eb2d58207735d0 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 18 Jun 2025 11:57:59 +0200 Subject: [PATCH 09/25] test(node): Unflake feature flag test (#16636) This seems flaky (e.g. https://github.com/getsentry/sentry-javascript/actions/runs/15727905218/job/44322221132?pr=16633), likely because they may be flushed at the same time I suppose! --- .../onError/withScope/scenario.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts index b6537011a87f..4ba9d63b9733 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts @@ -1,22 +1,30 @@ -import type { Scope } from '@sentry/node'; import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; +const flagsIntegration = Sentry.featureFlagsIntegration(); + Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, transport: loggingTransport, - integrations: [Sentry.featureFlagsIntegration()], + integrations: [flagsIntegration], }); -const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); -flagsIntegration?.addFeatureFlag('shared', true); +async function run(): Promise { + flagsIntegration.addFeatureFlag('shared', true); -Sentry.withScope((_scope: Scope) => { - flagsIntegration?.addFeatureFlag('forked', true); - flagsIntegration?.addFeatureFlag('shared', false); - Sentry.captureException(new Error('Error in forked scope')); -}); + Sentry.withScope(() => { + flagsIntegration.addFeatureFlag('forked', true); + flagsIntegration.addFeatureFlag('shared', false); + Sentry.captureException(new Error('Error in forked scope')); + }); + + await Sentry.flush(); + + flagsIntegration.addFeatureFlag('main', true); + + throw new Error('Error in main scope'); +} -flagsIntegration?.addFeatureFlag('main', true); -throw new Error('Error in main scope'); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); From 474a64cd6f7132046f100bce37557e94be62918b Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 18 Jun 2025 13:49:36 +0200 Subject: [PATCH 10/25] test(browser): Add test showing behavior of capturing built-in class instances (#16638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is not really ideal, but there is no great alternative for this that I can think of to handle this generically 😬 Now at least we have a test showing the current behavior! --- .../builtInClassInstance/subject.js | 1 + .../builtInClassInstance/test.ts | 20 +++++++++++++++++++ .../captureException/classInstance/test.ts | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js new file mode 100644 index 000000000000..556cd5babb09 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js @@ -0,0 +1 @@ +Sentry.captureException(new Response('test body')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts new file mode 100644 index 000000000000..4f93b7b87d44 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should capture a class instance of a built-in class', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + expect(eventData.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: '[object Response]', + mechanism: { + type: 'generic', + handled: true, + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts index 5cdd446a1510..19349532282e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts @@ -3,7 +3,7 @@ import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; -sentryTest('should capture an POJO', async ({ getLocalTestUrl, page }) => { +sentryTest('should capture an class instance', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); From a34ad665faa86920f4a5611857e45e2154922b70 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 18 Jun 2025 09:21:47 -0400 Subject: [PATCH 11/25] ref(node): Add `renameAttributeKey` helper to Vercel AI integration (#16620) This refactor helps clean up redundant code. ref https://github.com/getsentry/sentry-javascript/pull/16580#discussion_r2149368938 --- .../integrations/tracing/vercelai/index.ts | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index 5c7a0ed5d959..9557d62b5e04 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -182,14 +182,12 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { continue; } - if (attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE] != undefined) { - attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]; - delete attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]; - } - if (attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE] != undefined) { - attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE]; - delete attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE]; - } + renameAttributeKey( + attributes, + AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, + ); + renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE); if ( typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' && typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number' @@ -199,22 +197,10 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { } // Rename AI SDK attributes to standardized gen_ai attributes - if (attributes[AI_PROMPT_MESSAGES_ATTRIBUTE] != undefined) { - attributes['gen_ai.request.messages'] = attributes[AI_PROMPT_MESSAGES_ATTRIBUTE]; - delete attributes[AI_PROMPT_MESSAGES_ATTRIBUTE]; - } - if (attributes[AI_RESPONSE_TEXT_ATTRIBUTE] != undefined) { - attributes['gen_ai.response.text'] = attributes[AI_RESPONSE_TEXT_ATTRIBUTE]; - delete attributes[AI_RESPONSE_TEXT_ATTRIBUTE]; - } - if (attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE] != undefined) { - attributes['gen_ai.response.tool_calls'] = attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]; - delete attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]; - } - if (attributes[AI_PROMPT_TOOLS_ATTRIBUTE] != undefined) { - attributes['gen_ai.request.available_tools'] = attributes[AI_PROMPT_TOOLS_ATTRIBUTE]; - delete attributes[AI_PROMPT_TOOLS_ATTRIBUTE]; - } + renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages'); + renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); + renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); + renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools'); } } @@ -274,3 +260,14 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { * }); */ export const vercelAIIntegration = defineIntegration(_vercelAIIntegration); + +/** + * Renames an attribute key in the provided attributes object if the old key exists. + * This function safely handles null and undefined values. + */ +function renameAttributeKey(attributes: Record, oldKey: string, newKey: string): void { + if (attributes[oldKey] != null) { + attributes[newKey] = attributes[oldKey]; + delete attributes[oldKey]; + } +} From ceb003c15973c2d8f437dfb7025eedffbc8bc8b0 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 18 Jun 2025 15:42:24 +0200 Subject: [PATCH 12/25] ref(core): Remove `utils-hoist` folder (#16639) This was a leftover from migrating of the `@sentry/utils` package to core. This PR merges the former utils together into the `core/src/utils` directory, which should make this a bit easier to follow/find. --- packages/core/.eslintrc.js | 2 +- packages/core/src/api.ts | 2 +- .../core/src/asyncContext/stackStrategy.ts | 2 +- packages/core/src/breadcrumbs.ts | 4 +- packages/core/src/carrier.ts | 6 +- packages/core/src/checkin.ts | 4 +- packages/core/src/client.ts | 14 +-- packages/core/src/currentScopes.ts | 2 +- packages/core/src/envelope.ts | 8 +- packages/core/src/eventProcessors.ts | 6 +- packages/core/src/exports.ts | 10 +- packages/core/src/fetch.ts | 6 +- packages/core/src/index.ts | 104 ++++++++---------- .../{utils-hoist => }/instrument/console.ts | 8 +- .../src/{utils-hoist => }/instrument/fetch.ts | 14 +-- .../instrument/globalError.ts | 4 +- .../instrument/globalUnhandledRejection.ts | 4 +- .../{utils-hoist => }/instrument/handlers.ts | 6 +- packages/core/src/integration.ts | 2 +- .../core/src/integrations/captureconsole.ts | 12 +- packages/core/src/integrations/console.ts | 10 +- packages/core/src/integrations/dedupe.ts | 4 +- .../core/src/integrations/eventFilters.ts | 6 +- .../core/src/integrations/extraerrordata.ts | 10 +- .../core/src/integrations/functiontostring.ts | 2 +- .../core/src/integrations/linkederrors.ts | 4 +- packages/core/src/integrations/metadata.ts | 2 +- .../core/src/integrations/rewriteframes.ts | 4 +- packages/core/src/integrations/supabase.ts | 4 +- .../integrations/third-party-errors-filter.ts | 4 +- packages/core/src/integrations/zoderrors.ts | 4 +- packages/core/src/logs/console-integration.ts | 8 +- packages/core/src/logs/envelope.ts | 4 +- packages/core/src/logs/exports.ts | 8 +- packages/core/src/mcp-server.ts | 2 +- packages/core/src/metadata.ts | 2 +- packages/core/src/profiling.ts | 2 +- packages/core/src/scope.ts | 12 +- packages/core/src/sdk.ts | 2 +- packages/core/src/server-runtime-client.ts | 10 +- packages/core/src/session.ts | 4 +- .../src/tracing/dynamicSamplingContext.ts | 9 +- packages/core/src/tracing/errors.ts | 6 +- packages/core/src/tracing/idleSpan.ts | 4 +- packages/core/src/tracing/logSpans.ts | 2 +- packages/core/src/tracing/measurement.ts | 2 +- packages/core/src/tracing/sampling.ts | 2 +- .../src/tracing/sentryNonRecordingSpan.ts | 2 +- packages/core/src/tracing/sentrySpan.ts | 6 +- packages/core/src/tracing/trace.ts | 6 +- packages/core/src/tracing/utils.ts | 2 +- packages/core/src/transports/base.ts | 10 +- packages/core/src/transports/multiplexed.ts | 4 +- packages/core/src/transports/offline.ts | 6 +- packages/core/src/trpc.ts | 4 +- .../aggregate-errors.ts | 0 .../core/src/{utils-hoist => utils}/anr.ts | 0 .../src/{utils-hoist => utils}/baggage.ts | 2 +- .../breadcrumb-log-level.ts | 0 .../src/{utils-hoist => utils}/browser.ts | 0 .../{utils-hoist => utils}/clientreport.ts | 0 .../src/{utils-hoist => utils}/debug-ids.ts | 0 .../core/src/{utils-hoist => utils}/dsn.ts | 2 +- .../core/src/{utils-hoist => utils}/env.ts | 0 .../src/{utils-hoist => utils}/envelope.ts | 0 .../core/src/{utils-hoist => utils}/error.ts | 0 .../{utils-hoist => utils}/eventbuilder.ts | 0 packages/core/src/utils/featureFlags.ts | 4 +- .../core/src/utils/handleCallbackErrors.ts | 2 +- .../core/src/{utils-hoist => utils}/is.ts | 0 .../src/{utils-hoist => utils}/isBrowser.ts | 0 .../core/src/{utils-hoist => utils}/logger.ts | 2 +- .../core/src/{utils-hoist => utils}/lru.ts | 0 .../core/src/{utils-hoist => utils}/misc.ts | 0 .../node-stack-trace.ts | 0 .../core/src/{utils-hoist => utils}/node.ts | 0 .../src/{utils-hoist => utils}/normalize.ts | 0 .../core/src/{utils-hoist => utils}/object.ts | 2 +- .../core/src/{utils-hoist => utils}/path.ts | 0 packages/core/src/utils/prepareEvent.ts | 10 +- .../{utils-hoist => utils}/promisebuffer.ts | 0 .../propagationContext.ts | 0 .../src/{utils-hoist => utils}/ratelimit.ts | 0 packages/core/src/utils/sdkMetadata.ts | 2 +- .../src/{utils-hoist => utils}/severity.ts | 0 packages/core/src/utils/spanOnScope.ts | 2 +- packages/core/src/utils/spanUtils.ts | 10 +- .../src/{utils-hoist => utils}/stacktrace.ts | 0 .../core/src/{utils-hoist => utils}/string.ts | 2 +- .../src/{utils-hoist => utils}/supports.ts | 2 +- .../src/{utils-hoist => utils}/syncpromise.ts | 0 .../core/src/{utils-hoist => utils}/time.ts | 0 packages/core/src/utils/traceData.ts | 6 +- .../src/{utils-hoist => utils}/tracing.ts | 2 +- .../core/src/{utils-hoist => utils}/url.ts | 0 .../{utils-hoist => utils}/vercelWaitUntil.ts | 0 .../src/{utils-hoist => utils}/version.ts | 0 .../src/{utils-hoist => utils}/worldwide.ts | 0 .../vendor/escapeStringForRegex.ts | 0 packages/core/test/clear-global-scope.ts | 0 packages/core/test/lib/api.test.ts | 2 +- packages/core/test/lib/attachments.test.ts | 2 +- packages/core/test/lib/carrier.test.ts | 2 +- packages/core/test/lib/clear-global-scope.ts | 7 -- packages/core/test/lib/client.test.ts | 10 +- .../instrument/fetch.test.ts | 2 +- .../instrument/handlers.test.ts} | 2 +- packages/core/test/lib/integration.test.ts | 2 +- .../lib/integrations/captureconsole.test.ts | 8 +- .../third-party-errors-filter.test.ts | 6 +- packages/core/test/lib/logs/envelope.test.ts | 10 +- packages/core/test/lib/logs/exports.test.ts | 2 +- packages/core/test/lib/metadata.test.ts | 6 +- packages/core/test/lib/prepareEvent.test.ts | 2 +- packages/core/test/lib/scope.test.ts | 2 +- packages/core/test/lib/session.test.ts | 2 +- packages/core/test/lib/tracing/errors.test.ts | 4 +- .../core/test/lib/tracing/sentrySpan.test.ts | 2 +- .../core/test/lib/transports/base.test.ts | 6 +- .../utils}/aggregate-errors.test.ts | 12 +- .../utils}/baggage.test.ts | 2 +- .../utils}/breadcrumb-log-level.test.ts | 2 +- .../utils}/browser.test.ts | 2 +- .../utils}/clientreport.test.ts | 6 +- .../{utils-hoist => lib/utils}/dsn.test.ts | 6 +- .../utils}/envelope.test.ts | 14 +-- .../utils}/eventbuilder.test.ts | 8 +- .../core/test/lib/utils/featureFlags.test.ts | 2 +- .../{utils-hoist => lib/utils}/is.test.ts | 8 +- .../{utils-hoist => lib/utils}/lru.test.ts | 2 +- .../{utils-hoist => lib/utils}/misc.test.ts | 8 +- .../utils}/normalize-url.test.ts | 2 +- .../utils}/normalize.test.ts | 6 +- .../{utils-hoist => lib/utils}/object.test.ts | 6 +- .../{utils-hoist => lib/utils}/path.test.ts | 2 +- .../utils}/promisebuffer.test.ts | 4 +- .../utils}/ratelimit.test.ts | 4 +- .../utils}/severity.test.ts | 2 +- .../utils}/stacktrace.test.ts | 4 +- .../{utils-hoist => lib/utils}/string.test.ts | 2 +- .../utils}/supports.test.ts | 2 +- .../utils}/syncpromise.test.ts | 2 +- .../utils}/tracing.test.ts | 2 +- .../{utils-hoist => lib/utils}/url.test.ts | 2 +- .../utils}/vercelWaitUntil.test.ts | 4 +- .../utils}/worldwide.test.ts | 2 +- packages/core/test/mocks/client.ts | 2 +- packages/core/test/mocks/transport.ts | 2 +- .../core/test/{utils-hoist => }/testutils.ts | 7 ++ .../{utils-hoist => }/types/typedef.test.ts | 0 packages/node/test/integration/scope.test.ts | 3 +- 151 files changed, 320 insertions(+), 334 deletions(-) rename packages/core/src/{utils-hoist => }/instrument/console.ts (83%) rename packages/core/src/{utils-hoist => }/instrument/fetch.ts (95%) rename packages/core/src/{utils-hoist => }/instrument/globalError.ts (92%) rename packages/core/src/{utils-hoist => }/instrument/globalUnhandledRejection.ts (91%) rename packages/core/src/{utils-hoist => }/instrument/handlers.ts (93%) rename packages/core/src/{utils-hoist => utils}/aggregate-errors.ts (100%) rename packages/core/src/{utils-hoist => utils}/anr.ts (100%) rename packages/core/src/{utils-hoist => utils}/baggage.ts (99%) rename packages/core/src/{utils-hoist => utils}/breadcrumb-log-level.ts (100%) rename packages/core/src/{utils-hoist => utils}/browser.ts (100%) rename packages/core/src/{utils-hoist => utils}/clientreport.ts (100%) rename packages/core/src/{utils-hoist => utils}/debug-ids.ts (100%) rename packages/core/src/{utils-hoist => utils}/dsn.ts (98%) rename packages/core/src/{utils-hoist => utils}/env.ts (100%) rename packages/core/src/{utils-hoist => utils}/envelope.ts (100%) rename packages/core/src/{utils-hoist => utils}/error.ts (100%) rename packages/core/src/{utils-hoist => utils}/eventbuilder.ts (100%) rename packages/core/src/{utils-hoist => utils}/is.ts (100%) rename packages/core/src/{utils-hoist => utils}/isBrowser.ts (100%) rename packages/core/src/{utils-hoist => utils}/logger.ts (98%) rename packages/core/src/{utils-hoist => utils}/lru.ts (100%) rename packages/core/src/{utils-hoist => utils}/misc.ts (100%) rename packages/core/src/{utils-hoist => utils}/node-stack-trace.ts (100%) rename packages/core/src/{utils-hoist => utils}/node.ts (100%) rename packages/core/src/{utils-hoist => utils}/normalize.ts (100%) rename packages/core/src/{utils-hoist => utils}/object.ts (99%) rename packages/core/src/{utils-hoist => utils}/path.ts (100%) rename packages/core/src/{utils-hoist => utils}/promisebuffer.ts (100%) rename packages/core/src/{utils-hoist => utils}/propagationContext.ts (100%) rename packages/core/src/{utils-hoist => utils}/ratelimit.ts (100%) rename packages/core/src/{utils-hoist => utils}/severity.ts (100%) rename packages/core/src/{utils-hoist => utils}/stacktrace.ts (100%) rename packages/core/src/{utils-hoist => utils}/string.ts (98%) rename packages/core/src/{utils-hoist => utils}/supports.ts (98%) rename packages/core/src/{utils-hoist => utils}/syncpromise.ts (100%) rename packages/core/src/{utils-hoist => utils}/time.ts (100%) rename packages/core/src/{utils-hoist => utils}/tracing.ts (98%) rename packages/core/src/{utils-hoist => utils}/url.ts (100%) rename packages/core/src/{utils-hoist => utils}/vercelWaitUntil.ts (100%) rename packages/core/src/{utils-hoist => utils}/version.ts (100%) rename packages/core/src/{utils-hoist => utils}/worldwide.ts (100%) rename packages/core/src/{utils-hoist => }/vendor/escapeStringForRegex.ts (100%) create mode 100644 packages/core/test/clear-global-scope.ts delete mode 100644 packages/core/test/lib/clear-global-scope.ts rename packages/core/test/{utils-hoist => lib}/instrument/fetch.test.ts (93%) rename packages/core/test/{utils-hoist/instrument.test.ts => lib/instrument/handlers.test.ts} (81%) rename packages/core/test/{utils-hoist => lib/utils}/aggregate-errors.test.ts (95%) rename packages/core/test/{utils-hoist => lib/utils}/baggage.test.ts (98%) rename packages/core/test/{utils-hoist => lib/utils}/breadcrumb-log-level.test.ts (82%) rename packages/core/test/{utils-hoist => lib/utils}/browser.test.ts (97%) rename packages/core/test/{utils-hoist => lib/utils}/clientreport.test.ts (88%) rename packages/core/test/{utils-hoist => lib/utils}/dsn.test.ts (98%) rename packages/core/test/{utils-hoist => lib/utils}/envelope.test.ts (94%) rename packages/core/test/{utils-hoist => lib/utils}/eventbuilder.test.ts (95%) rename packages/core/test/{utils-hoist => lib/utils}/is.test.ts (95%) rename packages/core/test/{utils-hoist => lib/utils}/lru.test.ts (94%) rename packages/core/test/{utils-hoist => lib/utils}/misc.test.ts (97%) rename packages/core/test/{utils-hoist => lib/utils}/normalize-url.test.ts (97%) rename packages/core/test/{utils-hoist => lib/utils}/normalize.test.ts (99%) rename packages/core/test/{utils-hoist => lib/utils}/object.test.ts (98%) rename packages/core/test/{utils-hoist => lib/utils}/path.test.ts (95%) rename packages/core/test/{utils-hoist => lib/utils}/promisebuffer.test.ts (96%) rename packages/core/test/{utils-hoist => lib/utils}/ratelimit.test.ts (98%) rename packages/core/test/{utils-hoist => lib/utils}/severity.test.ts (87%) rename packages/core/test/{utils-hoist => lib/utils}/stacktrace.test.ts (98%) rename packages/core/test/{utils-hoist => lib/utils}/string.test.ts (99%) rename packages/core/test/{utils-hoist => lib/utils}/supports.test.ts (93%) rename packages/core/test/{utils-hoist => lib/utils}/syncpromise.test.ts (99%) rename packages/core/test/{utils-hoist => lib/utils}/tracing.test.ts (99%) rename packages/core/test/{utils-hoist => lib/utils}/url.test.ts (99%) rename packages/core/test/{utils-hoist => lib/utils}/vercelWaitUntil.test.ts (90%) rename packages/core/test/{utils-hoist => lib/utils}/worldwide.test.ts (85%) rename packages/core/test/{utils-hoist => }/testutils.ts (65%) rename packages/core/test/{utils-hoist => }/types/typedef.test.ts (100%) diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js index 5c0b9f20f693..5a021c016763 100644 --- a/packages/core/.eslintrc.js +++ b/packages/core/.eslintrc.js @@ -1,4 +1,4 @@ module.exports = { extends: ['../../.eslintrc.js'], - ignorePatterns: ['rollup.npm.config.mjs', 'test/utils-hoist/buildPolyfills/originals.js'], + ignorePatterns: ['rollup.npm.config.mjs'], }; diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 9a83e0667476..924c6a8e28ad 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,7 +1,7 @@ import type { ReportDialogOptions } from './report-dialog'; import type { DsnComponents, DsnLike } from './types-hoist/dsn'; import type { SdkInfo } from './types-hoist/sdkinfo'; -import { dsnToString, makeDsn } from './utils-hoist/dsn'; +import { dsnToString, makeDsn } from './utils/dsn'; const SENTRY_API_VERSION = '7'; diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index cb0bf878b39c..845605be731d 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -1,7 +1,7 @@ import type { Client } from '../client'; import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScopes'; import { Scope } from '../scope'; -import { isThenable } from '../utils-hoist/is'; +import { isThenable } from '../utils/is'; import { getMainCarrier, getSentryCarrier } from './../carrier'; import type { AsyncContextStrategy } from './types'; diff --git a/packages/core/src/breadcrumbs.ts b/packages/core/src/breadcrumbs.ts index 5724de09be0a..78157a40e2e3 100644 --- a/packages/core/src/breadcrumbs.ts +++ b/packages/core/src/breadcrumbs.ts @@ -1,7 +1,7 @@ import { getClient, getIsolationScope } from './currentScopes'; import type { Breadcrumb, BreadcrumbHint } from './types-hoist/breadcrumb'; -import { consoleSandbox } from './utils-hoist/logger'; -import { dateTimestampInSeconds } from './utils-hoist/time'; +import { consoleSandbox } from './utils/logger'; +import { dateTimestampInSeconds } from './utils/time'; /** * Default maximum number of breadcrumbs added to an event. Can be overwritten diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts index 5136121cb8ae..d11ef2940d0c 100644 --- a/packages/core/src/carrier.ts +++ b/packages/core/src/carrier.ts @@ -1,9 +1,9 @@ import type { AsyncContextStack } from './asyncContext/stackStrategy'; import type { AsyncContextStrategy } from './asyncContext/types'; import type { Scope } from './scope'; -import type { Logger } from './utils-hoist/logger'; -import { SDK_VERSION } from './utils-hoist/version'; -import { GLOBAL_OBJ } from './utils-hoist/worldwide'; +import type { Logger } from './utils/logger'; +import { SDK_VERSION } from './utils/version'; +import { GLOBAL_OBJ } from './utils/worldwide'; /** * An object that contains globally accessible properties and maintains a scope stack. diff --git a/packages/core/src/checkin.ts b/packages/core/src/checkin.ts index 424dfb2badda..7037c9ebadb2 100644 --- a/packages/core/src/checkin.ts +++ b/packages/core/src/checkin.ts @@ -2,8 +2,8 @@ import type { SerializedCheckIn } from './types-hoist/checkin'; import type { DsnComponents } from './types-hoist/dsn'; import type { CheckInEnvelope, CheckInItem, DynamicSamplingContext } from './types-hoist/envelope'; import type { SdkMetadata } from './types-hoist/sdkmetadata'; -import { dsnToString } from './utils-hoist/dsn'; -import { createEnvelope } from './utils-hoist/envelope'; +import { dsnToString } from './utils/dsn'; +import { createEnvelope } from './utils/envelope'; /** * Create envelope from check in item. diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index d9abd8f5d0d2..3dd8bd66d023 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -32,19 +32,19 @@ import type { SeverityLevel } from './types-hoist/severity'; import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-hoist/span'; import type { StartSpanOptions } from './types-hoist/startSpanOptions'; import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport'; +import { createClientReportEnvelope } from './utils/clientreport'; +import { dsnToString, makeDsn } from './utils/dsn'; +import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils/envelope'; import { getPossibleEventMessages } from './utils/eventUtils'; +import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils/is'; +import { logger } from './utils/logger'; import { merge } from './utils/merge'; +import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; +import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils/syncpromise'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; -import { createClientReportEnvelope } from './utils-hoist/clientreport'; -import { dsnToString, makeDsn } from './utils-hoist/dsn'; -import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope'; -import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc'; -import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils-hoist/syncpromise'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release'; diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts index b8d719098e49..fc40051e56d8 100644 --- a/packages/core/src/currentScopes.ts +++ b/packages/core/src/currentScopes.ts @@ -3,7 +3,7 @@ import { getGlobalSingleton, getMainCarrier } from './carrier'; import type { Client } from './client'; import { Scope } from './scope'; import type { TraceContext } from './types-hoist/context'; -import { generateSpanId } from './utils-hoist/propagationContext'; +import { generateSpanId } from './utils/propagationContext'; /** * Get the currently active scope. diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index efec02eb7323..dc094d218812 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -18,15 +18,15 @@ import type { Event } from './types-hoist/event'; import type { SdkInfo } from './types-hoist/sdkinfo'; import type { SdkMetadata } from './types-hoist/sdkmetadata'; import type { Session, SessionAggregates } from './types-hoist/session'; -import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; -import { dsnToString } from './utils-hoist/dsn'; +import { dsnToString } from './utils/dsn'; import { createEnvelope, createEventEnvelopeHeaders, createSpanEnvelopeItem, getSdkMetadataForEnvelopeHeader, -} from './utils-hoist/envelope'; -import { uuid4 } from './utils-hoist/misc'; +} from './utils/envelope'; +import { uuid4 } from './utils/misc'; +import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; /** * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. diff --git a/packages/core/src/eventProcessors.ts b/packages/core/src/eventProcessors.ts index d2405a90032b..436f3e3c79ed 100644 --- a/packages/core/src/eventProcessors.ts +++ b/packages/core/src/eventProcessors.ts @@ -1,9 +1,9 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { EventProcessor } from './types-hoist/eventprocessor'; -import { isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { SyncPromise } from './utils-hoist/syncpromise'; +import { isThenable } from './utils/is'; +import { logger } from './utils/logger'; +import { SyncPromise } from './utils/syncpromise'; /** * Process an array of event processors, returning the processed event (or `null` if the event was dropped). diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d7e678b16edd..92e4d09d4d81 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -10,13 +10,13 @@ import type { Primitive } from './types-hoist/misc'; import type { Session, SessionContext } from './types-hoist/session'; import type { SeverityLevel } from './types-hoist/severity'; import type { User } from './types-hoist/user'; +import { isThenable } from './utils/is'; +import { logger } from './utils/logger'; +import { uuid4 } from './utils/misc'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; -import { isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { uuid4 } from './utils-hoist/misc'; -import { timestampInSeconds } from './utils-hoist/time'; -import { GLOBAL_OBJ } from './utils-hoist/worldwide'; +import { timestampInSeconds } from './utils/time'; +import { GLOBAL_OBJ } from './utils/worldwide'; /** * Captures an exception event and sends it to Sentry. diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 65274d1e82a3..cf82eca1e6c1 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -5,12 +5,12 @@ import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; import type { FetchBreadcrumbHint } from './types-hoist/breadcrumb'; import type { HandlerDataFetch } from './types-hoist/instrument'; import type { Span, SpanAttributes, SpanOrigin } from './types-hoist/span'; +import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils/baggage'; import { hasSpansEnabled } from './utils/hasSpansEnabled'; +import { isInstanceOf, isRequest } from './utils/is'; import { getActiveSpan } from './utils/spanUtils'; import { getTraceData } from './utils/traceData'; -import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; -import { isInstanceOf, isRequest } from './utils-hoist/is'; -import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils-hoist/url'; +import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils/url'; type PolymorphicRequestHeaders = | Record diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fdbcd8bffbef..b4f09d89f381 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -133,24 +133,19 @@ export { _INTERNAL_MAX_FLAGS_PER_SPAN, } from './utils/featureFlags'; -export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; -export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils-hoist/breadcrumb-log-level'; -export { getComponentName, getLocationHref, htmlTreeAsString } from './utils-hoist/browser'; -export { dsnFromString, dsnToString, makeDsn } from './utils-hoist/dsn'; +export { applyAggregateErrorsToEvent } from './utils/aggregate-errors'; +export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils/breadcrumb-log-level'; +export { getComponentName, getLocationHref, htmlTreeAsString } from './utils/browser'; +export { dsnFromString, dsnToString, makeDsn } from './utils/dsn'; // eslint-disable-next-line deprecation/deprecation -export { SentryError } from './utils-hoist/error'; -export { GLOBAL_OBJ } from './utils-hoist/worldwide'; -export type { InternalGlobal } from './utils-hoist/worldwide'; -export { addConsoleInstrumentationHandler } from './utils-hoist/instrument/console'; -export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './utils-hoist/instrument/fetch'; -export { addGlobalErrorInstrumentationHandler } from './utils-hoist/instrument/globalError'; -export { addGlobalUnhandledRejectionInstrumentationHandler } from './utils-hoist/instrument/globalUnhandledRejection'; -export { - addHandler, - maybeInstrument, - resetInstrumentationHandlers, - triggerHandlers, -} from './utils-hoist/instrument/handlers'; +export { SentryError } from './utils/error'; +export { GLOBAL_OBJ } from './utils/worldwide'; +export type { InternalGlobal } from './utils/worldwide'; +export { addConsoleInstrumentationHandler } from './instrument/console'; +export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; +export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; +export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; +export { addHandler, maybeInstrument, resetInstrumentationHandlers, triggerHandlers } from './instrument/handlers'; export { isDOMError, isDOMException, @@ -167,10 +162,10 @@ export { isSyntheticEvent, isThenable, isVueViewModel, -} from './utils-hoist/is'; -export { isBrowser } from './utils-hoist/isBrowser'; -export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './utils-hoist/logger'; -export type { Logger } from './utils-hoist/logger'; +} from './utils/is'; +export { isBrowser } from './utils/isBrowser'; +export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './utils/logger'; +export type { Logger } from './utils/logger'; export { addContextToFrame, addExceptionMechanism, @@ -179,9 +174,9 @@ export { getEventDescription, parseSemver, uuid4, -} from './utils-hoist/misc'; -export { isNodeEnv, loadModule } from './utils-hoist/node'; -export { normalize, normalizeToSize, normalizeUrlToBase } from './utils-hoist/normalize'; +} from './utils/misc'; +export { isNodeEnv, loadModule } from './utils/node'; +export { normalize, normalizeToSize, normalizeUrlToBase } from './utils/normalize'; export { addNonEnumerableProperty, convertToPlainObject, @@ -192,11 +187,11 @@ export { getOriginalFunction, markFunctionWrapped, objectify, -} from './utils-hoist/object'; -export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './utils-hoist/path'; -export { makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils-hoist/promisebuffer'; -export type { PromiseBuffer } from './utils-hoist/promisebuffer'; -export { severityLevelFromString } from './utils-hoist/severity'; +} from './utils/object'; +export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './utils/path'; +export { makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer'; +export type { PromiseBuffer } from './utils/promisebuffer'; +export { severityLevelFromString } from './utils/severity'; export { UNKNOWN_FUNCTION, createStackParser, @@ -204,9 +199,9 @@ export { getFunctionName, stackParserFromStackParserOptions, stripSentryFramesAndReverse, -} from './utils-hoist/stacktrace'; -export { filenameIsInApp, node, nodeStackLineParser } from './utils-hoist/node-stack-trace'; -export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils-hoist/string'; +} from './utils/stacktrace'; +export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace'; +export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils/string'; export { isNativeFunction, supportsDOMError, @@ -219,17 +214,17 @@ export { // eslint-disable-next-line deprecation/deprecation supportsReferrerPolicy, supportsReportingObserver, -} from './utils-hoist/supports'; -export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; -export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils-hoist/time'; +} from './utils/supports'; +export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise'; +export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils/time'; export { TRACEPARENT_REGEXP, extractTraceparentData, generateSentryTraceHeader, propagationContextFromHeaders, -} from './utils-hoist/tracing'; -export { getSDKSource, isBrowserBundle } from './utils-hoist/env'; -export type { SdkSource } from './utils-hoist/env'; +} from './utils/tracing'; +export { getSDKSource, isBrowserBundle } from './utils/env'; +export type { SdkSource } from './utils/env'; export { addItemToEnvelope, createAttachmentEnvelopeItem, @@ -242,16 +237,16 @@ export { getSdkMetadataForEnvelopeHeader, parseEnvelope, serializeEnvelope, -} from './utils-hoist/envelope'; -export { createClientReportEnvelope } from './utils-hoist/clientreport'; +} from './utils/envelope'; +export { createClientReportEnvelope } from './utils/clientreport'; export { DEFAULT_RETRY_AFTER, disabledUntil, isRateLimited, parseRetryAfterHeader, updateRateLimits, -} from './utils-hoist/ratelimit'; -export type { RateLimits } from './utils-hoist/ratelimit'; +} from './utils/ratelimit'; +export type { RateLimits } from './utils/ratelimit'; export { MAX_BAGGAGE_STRING_LENGTH, SENTRY_BAGGAGE_KEY_PREFIX, @@ -260,7 +255,7 @@ export { dynamicSamplingContextToSentryBaggageHeader, parseBaggageHeader, objectToBaggageHeader, -} from './utils-hoist/baggage'; +} from './utils/baggage'; export { getSanitizedUrlString, parseUrl, @@ -269,20 +264,15 @@ export { getHttpSpanDetailsFromUrlObject, isURLObjectRelative, getSanitizedUrlStringFromUrlObject, -} from './utils-hoist/url'; -export { - eventFromMessage, - eventFromUnknownInput, - exceptionFromError, - parseStackFrames, -} from './utils-hoist/eventbuilder'; -export { callFrameToStackFrame, watchdogTimer } from './utils-hoist/anr'; -export { LRUMap } from './utils-hoist/lru'; -export { generateTraceId, generateSpanId } from './utils-hoist/propagationContext'; -export { vercelWaitUntil } from './utils-hoist/vercelWaitUntil'; -export { SDK_VERSION } from './utils-hoist/version'; -export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils-hoist/debug-ids'; -export { escapeStringForRegex } from './utils-hoist/vendor/escapeStringForRegex'; +} from './utils/url'; +export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './utils/eventbuilder'; +export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; +export { LRUMap } from './utils/lru'; +export { generateTraceId, generateSpanId } from './utils/propagationContext'; +export { vercelWaitUntil } from './utils/vercelWaitUntil'; +export { SDK_VERSION } from './utils/version'; +export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids'; +export { escapeStringForRegex } from './vendor/escapeStringForRegex'; export type { Attachment } from './types-hoist/attachment'; export type { diff --git a/packages/core/src/utils-hoist/instrument/console.ts b/packages/core/src/instrument/console.ts similarity index 83% rename from packages/core/src/utils-hoist/instrument/console.ts rename to packages/core/src/instrument/console.ts index 90fad9fbde43..141f3bcb33c9 100644 --- a/packages/core/src/utils-hoist/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ -import type { ConsoleLevel, HandlerDataConsole } from '../../types-hoist/instrument'; -import { CONSOLE_LEVELS, originalConsoleMethods } from '../logger'; -import { fill } from '../object'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { ConsoleLevel, HandlerDataConsole } from '../types-hoist/instrument'; +import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/logger'; +import { fill } from '../utils/object'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; /** diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/instrument/fetch.ts similarity index 95% rename from packages/core/src/utils-hoist/instrument/fetch.ts rename to packages/core/src/instrument/fetch.ts index 270219f80efb..be079280215a 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/instrument/fetch.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { HandlerDataFetch } from '../../types-hoist/instrument'; -import type { WebFetchHeaders } from '../../types-hoist/webfetchapi'; -import { isError, isRequest } from '../is'; -import { addNonEnumerableProperty, fill } from '../object'; -import { supportsNativeFetch } from '../supports'; -import { timestampInSeconds } from '../time'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { HandlerDataFetch } from '../types-hoist/instrument'; +import type { WebFetchHeaders } from '../types-hoist/webfetchapi'; +import { isError, isRequest } from '../utils/is'; +import { addNonEnumerableProperty, fill } from '../utils/object'; +import { supportsNativeFetch } from '../utils/supports'; +import { timestampInSeconds } from '../utils/time'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; type FetchResource = string | { toString(): string } | { url: string }; diff --git a/packages/core/src/utils-hoist/instrument/globalError.ts b/packages/core/src/instrument/globalError.ts similarity index 92% rename from packages/core/src/utils-hoist/instrument/globalError.ts rename to packages/core/src/instrument/globalError.ts index 6f4f23cb65b7..39e00ed816c4 100644 --- a/packages/core/src/utils-hoist/instrument/globalError.ts +++ b/packages/core/src/instrument/globalError.ts @@ -1,5 +1,5 @@ -import type { HandlerDataError } from '../../types-hoist/instrument'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { HandlerDataError } from '../types-hoist/instrument'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; let _oldOnErrorHandler: (typeof GLOBAL_OBJ)['onerror'] | null = null; diff --git a/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts b/packages/core/src/instrument/globalUnhandledRejection.ts similarity index 91% rename from packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts rename to packages/core/src/instrument/globalUnhandledRejection.ts index bbf43c949394..230146ae8646 100644 --- a/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts +++ b/packages/core/src/instrument/globalUnhandledRejection.ts @@ -1,5 +1,5 @@ -import type { HandlerDataUnhandledRejection } from '../../types-hoist/instrument'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { HandlerDataUnhandledRejection } from '../types-hoist/instrument'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; let _oldOnUnhandledRejectionHandler: (typeof GLOBAL_OBJ)['onunhandledrejection'] | null = null; diff --git a/packages/core/src/utils-hoist/instrument/handlers.ts b/packages/core/src/instrument/handlers.ts similarity index 93% rename from packages/core/src/utils-hoist/instrument/handlers.ts rename to packages/core/src/instrument/handlers.ts index 9d6222662912..2d88b3d8abe9 100644 --- a/packages/core/src/utils-hoist/instrument/handlers.ts +++ b/packages/core/src/instrument/handlers.ts @@ -1,6 +1,6 @@ -import { DEBUG_BUILD } from '../../debug-build'; -import { logger } from '../logger'; -import { getFunctionName } from '../stacktrace'; +import { DEBUG_BUILD } from '../debug-build'; +import { logger } from '../utils/logger'; +import { getFunctionName } from '../utils/stacktrace'; export type InstrumentHandlerType = | 'console' diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index c5636d002cc1..d8d741183287 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -4,7 +4,7 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { Integration, IntegrationFn } from './types-hoist/integration'; import type { Options } from './types-hoist/options'; -import { logger } from './utils-hoist/logger'; +import { logger } from './utils/logger'; export const installedIntegrations: string[] = []; diff --git a/packages/core/src/integrations/captureconsole.ts b/packages/core/src/integrations/captureconsole.ts index e5dc43200dfe..83863349e14a 100644 --- a/packages/core/src/integrations/captureconsole.ts +++ b/packages/core/src/integrations/captureconsole.ts @@ -1,14 +1,14 @@ import { getClient, withScope } from '../currentScopes'; import { captureException, captureMessage } from '../exports'; +import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { CaptureContext } from '../scope'; import type { IntegrationFn } from '../types-hoist/integration'; -import { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; -import { CONSOLE_LEVELS } from '../utils-hoist/logger'; -import { addExceptionMechanism } from '../utils-hoist/misc'; -import { severityLevelFromString } from '../utils-hoist/severity'; -import { safeJoin } from '../utils-hoist/string'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { CONSOLE_LEVELS } from '../utils/logger'; +import { addExceptionMechanism } from '../utils/misc'; +import { severityLevelFromString } from '../utils/severity'; +import { safeJoin } from '../utils/string'; +import { GLOBAL_OBJ } from '../utils/worldwide'; interface CaptureConsoleOptions { levels?: string[]; diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index e995056a1f23..3aa1cd8d9f3e 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -1,12 +1,12 @@ import { addBreadcrumb } from '../breadcrumbs'; import { getClient } from '../currentScopes'; +import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { ConsoleLevel } from '../types-hoist/instrument'; -import { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; -import { CONSOLE_LEVELS } from '../utils-hoist/logger'; -import { severityLevelFromString } from '../utils-hoist/severity'; -import { safeJoin } from '../utils-hoist/string'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { CONSOLE_LEVELS } from '../utils/logger'; +import { severityLevelFromString } from '../utils/severity'; +import { safeJoin } from '../utils/string'; +import { GLOBAL_OBJ } from '../utils/worldwide'; interface ConsoleIntegrationOptions { levels: ConsoleLevel[]; diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 9b2fae754e28..ab0e7eb41b25 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -4,8 +4,8 @@ import type { Event } from '../types-hoist/event'; import type { Exception } from '../types-hoist/exception'; import type { IntegrationFn } from '../types-hoist/integration'; import type { StackFrame } from '../types-hoist/stackframe'; -import { logger } from '../utils-hoist/logger'; -import { getFramesFromEvent } from '../utils-hoist/stacktrace'; +import { logger } from '../utils/logger'; +import { getFramesFromEvent } from '../utils/stacktrace'; const INTEGRATION_NAME = 'Dedupe'; diff --git a/packages/core/src/integrations/eventFilters.ts b/packages/core/src/integrations/eventFilters.ts index 7e254c16f0fa..c1818c8a46f8 100644 --- a/packages/core/src/integrations/eventFilters.ts +++ b/packages/core/src/integrations/eventFilters.ts @@ -4,9 +4,9 @@ import type { Event } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; import type { StackFrame } from '../types-hoist/stackframe'; import { getPossibleEventMessages } from '../utils/eventUtils'; -import { logger } from '../utils-hoist/logger'; -import { getEventDescription } from '../utils-hoist/misc'; -import { stringMatchesSomePattern } from '../utils-hoist/string'; +import { logger } from '../utils/logger'; +import { getEventDescription } from '../utils/misc'; +import { stringMatchesSomePattern } from '../utils/string'; // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. diff --git a/packages/core/src/integrations/extraerrordata.ts b/packages/core/src/integrations/extraerrordata.ts index d58d116783f3..3856699c9f68 100644 --- a/packages/core/src/integrations/extraerrordata.ts +++ b/packages/core/src/integrations/extraerrordata.ts @@ -4,11 +4,11 @@ import type { Contexts } from '../types-hoist/context'; import type { ExtendedError } from '../types-hoist/error'; import type { Event, EventHint } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; -import { isError, isPlainObject } from '../utils-hoist/is'; -import { logger } from '../utils-hoist/logger'; -import { normalize } from '../utils-hoist/normalize'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; -import { truncate } from '../utils-hoist/string'; +import { isError, isPlainObject } from '../utils/is'; +import { logger } from '../utils/logger'; +import { normalize } from '../utils/normalize'; +import { addNonEnumerableProperty } from '../utils/object'; +import { truncate } from '../utils/string'; const INTEGRATION_NAME = 'ExtraErrorData'; diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index aef1b95795cd..5426e8291cb6 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -3,7 +3,7 @@ import { getClient } from '../currentScopes'; import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types-hoist/integration'; import type { WrappedFunction } from '../types-hoist/wrappedfunction'; -import { getOriginalFunction } from '../utils-hoist/object'; +import { getOriginalFunction } from '../utils/object'; let originalFunctionToString: () => void; diff --git a/packages/core/src/integrations/linkederrors.ts b/packages/core/src/integrations/linkederrors.ts index c9263eba744d..a45837be7548 100644 --- a/packages/core/src/integrations/linkederrors.ts +++ b/packages/core/src/integrations/linkederrors.ts @@ -1,7 +1,7 @@ import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types-hoist/integration'; -import { applyAggregateErrorsToEvent } from '../utils-hoist/aggregate-errors'; -import { exceptionFromError } from '../utils-hoist/eventbuilder'; +import { applyAggregateErrorsToEvent } from '../utils/aggregate-errors'; +import { exceptionFromError } from '../utils/eventbuilder'; interface LinkedErrorsOptions { key?: string; diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts index 2a00d4d48137..1bbbbdfb9624 100644 --- a/packages/core/src/integrations/metadata.ts +++ b/packages/core/src/integrations/metadata.ts @@ -1,7 +1,7 @@ import { defineIntegration } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; import type { EventItem } from '../types-hoist/envelope'; -import { forEachEnvelopeItem } from '../utils-hoist/envelope'; +import { forEachEnvelopeItem } from '../utils/envelope'; /** * Adds module metadata to stack frames. diff --git a/packages/core/src/integrations/rewriteframes.ts b/packages/core/src/integrations/rewriteframes.ts index 8b7361e40d94..c903016b0531 100644 --- a/packages/core/src/integrations/rewriteframes.ts +++ b/packages/core/src/integrations/rewriteframes.ts @@ -2,8 +2,8 @@ import { defineIntegration } from '../integration'; import type { Event } from '../types-hoist/event'; import type { StackFrame } from '../types-hoist/stackframe'; import type { Stacktrace } from '../types-hoist/stacktrace'; -import { basename, relative } from '../utils-hoist/path'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { basename, relative } from '../utils/path'; +import { GLOBAL_OBJ } from '../utils/worldwide'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 084d50356a83..ac781e95ece6 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -9,8 +9,8 @@ import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import { setHttpStatus, SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan } from '../tracing'; import type { IntegrationFn } from '../types-hoist/integration'; -import { isPlainObject } from '../utils-hoist/is'; -import { logger } from '../utils-hoist/logger'; +import { isPlainObject } from '../utils/is'; +import { logger } from '../utils/logger'; const AUTH_OPERATIONS_TO_INSTRUMENT = [ 'reauthenticate', diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts index b61e6ccffb45..1a0628359f5b 100644 --- a/packages/core/src/integrations/third-party-errors-filter.ts +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -2,8 +2,8 @@ import { defineIntegration } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; import type { EventItem } from '../types-hoist/envelope'; import type { Event } from '../types-hoist/event'; -import { forEachEnvelopeItem } from '../utils-hoist/envelope'; -import { getFramesFromEvent } from '../utils-hoist/stacktrace'; +import { forEachEnvelopeItem } from '../utils/envelope'; +import { getFramesFromEvent } from '../utils/stacktrace'; interface Options { /** diff --git a/packages/core/src/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts index 315b24592096..50c9c9e0237e 100644 --- a/packages/core/src/integrations/zoderrors.ts +++ b/packages/core/src/integrations/zoderrors.ts @@ -1,8 +1,8 @@ import { defineIntegration } from '../integration'; import type { Event, EventHint } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; -import { isError } from '../utils-hoist/is'; -import { truncate } from '../utils-hoist/string'; +import { isError } from '../utils/is'; +import { truncate } from '../utils/string'; interface ZodErrorsOptions { key?: string; diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index 9d64439e1786..58460f59d9ab 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -1,13 +1,13 @@ import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; +import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { ConsoleLevel } from '../types-hoist/instrument'; import type { IntegrationFn } from '../types-hoist/integration'; -import { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; -import { CONSOLE_LEVELS, logger } from '../utils-hoist/logger'; -import { safeJoin } from '../utils-hoist/string'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { CONSOLE_LEVELS, logger } from '../utils/logger'; +import { safeJoin } from '../utils/string'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { _INTERNAL_captureLog } from './exports'; interface CaptureConsoleOptions { diff --git a/packages/core/src/logs/envelope.ts b/packages/core/src/logs/envelope.ts index c909a9140de1..c1d5b23e1575 100644 --- a/packages/core/src/logs/envelope.ts +++ b/packages/core/src/logs/envelope.ts @@ -2,8 +2,8 @@ import type { DsnComponents } from '../types-hoist/dsn'; import type { LogContainerItem, LogEnvelope } from '../types-hoist/envelope'; import type { SerializedLog } from '../types-hoist/log'; import type { SdkMetadata } from '../types-hoist/sdkmetadata'; -import { dsnToString } from '../utils-hoist/dsn'; -import { createEnvelope } from '../utils-hoist/envelope'; +import { dsnToString } from '../utils/dsn'; +import { createEnvelope } from '../utils/envelope'; /** * Creates a log container envelope item for a list of logs. diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 176cdcd657b0..0ec4d960c66d 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -5,11 +5,11 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Scope, ScopeData } from '../scope'; import type { Log, SerializedLog, SerializedLogAttributeValue } from '../types-hoist/log'; import { mergeScopeData } from '../utils/applyScopeDataToEvent'; +import { isParameterizedString } from '../utils/is'; +import { logger } from '../utils/logger'; import { _getSpanForScope } from '../utils/spanOnScope'; -import { isParameterizedString } from '../utils-hoist/is'; -import { logger } from '../utils-hoist/logger'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { timestampInSeconds } from '../utils/time'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; import { createLogEnvelope } from './envelope'; diff --git a/packages/core/src/mcp-server.ts b/packages/core/src/mcp-server.ts index bd3d83795d1c..1a6f626a83f2 100644 --- a/packages/core/src/mcp-server.ts +++ b/packages/core/src/mcp-server.ts @@ -6,8 +6,8 @@ import { } from './semanticAttributes'; import { startSpan, withActiveSpan } from './tracing'; import type { Span } from './types-hoist/span'; +import { logger } from './utils/logger'; import { getActiveSpan } from './utils/spanUtils'; -import { logger } from './utils-hoist/logger'; interface MCPTransport { // The first argument is a JSON RPC message diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 8eec6542a7d4..c88d7a36406d 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -1,6 +1,6 @@ import type { Event } from './types-hoist/event'; import type { StackParser } from './types-hoist/stacktrace'; -import { GLOBAL_OBJ } from './utils-hoist/worldwide'; +import { GLOBAL_OBJ } from './utils/worldwide'; /** Keys are source filename/url, values are metadata objects. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 8e7da5d8e946..956f37992f81 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -1,7 +1,7 @@ import { getClient } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Profiler, ProfilingIntegration } from './types-hoist/profiling'; -import { logger } from './utils-hoist/logger'; +import { logger } from './utils/logger'; function isProfilingIntegrationWithProfiler( integration: ProfilingIntegration | undefined, diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 414f1e38a2df..6f9269faec78 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -15,14 +15,14 @@ import type { SeverityLevel } from './types-hoist/severity'; import type { Span } from './types-hoist/span'; import type { PropagationContext } from './types-hoist/tracing'; import type { User } from './types-hoist/user'; +import { isPlainObject } from './utils/is'; +import { logger } from './utils/logger'; import { merge } from './utils/merge'; +import { uuid4 } from './utils/misc'; +import { generateTraceId } from './utils/propagationContext'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; -import { isPlainObject } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { uuid4 } from './utils-hoist/misc'; -import { generateTraceId } from './utils-hoist/propagationContext'; -import { truncate } from './utils-hoist/string'; -import { dateTimestampInSeconds } from './utils-hoist/time'; +import { truncate } from './utils/string'; +import { dateTimestampInSeconds } from './utils/time'; /** * Default value for maximum number of breadcrumbs added to an event. diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index fa3194d1ebc5..3ebbcbd4b673 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -2,7 +2,7 @@ import type { Client } from './client'; import { getCurrentScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { ClientOptions } from './types-hoist/options'; -import { consoleSandbox, logger } from './utils-hoist/logger'; +import { consoleSandbox, logger } from './utils/logger'; /** A class object that can instantiate Client objects. */ export type ClientClass = new (options: O) => F; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 261c5b582c23..9f41e6142c24 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -13,11 +13,11 @@ import type { ClientOptions } from './types-hoist/options'; import type { ParameterizedString } from './types-hoist/parameterize'; import type { SeverityLevel } from './types-hoist/severity'; import type { BaseTransportOptions } from './types-hoist/transport'; -import { eventFromMessage, eventFromUnknownInput } from './utils-hoist/eventbuilder'; -import { isPrimitive } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { uuid4 } from './utils-hoist/misc'; -import { resolvedSyncPromise } from './utils-hoist/syncpromise'; +import { eventFromMessage, eventFromUnknownInput } from './utils/eventbuilder'; +import { isPrimitive } from './utils/is'; +import { logger } from './utils/logger'; +import { uuid4 } from './utils/misc'; +import { resolvedSyncPromise } from './utils/syncpromise'; // TODO: Make this configurable const DEFAULT_LOG_FLUSH_INTERVAL = 5000; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 116a00a9adbe..492378279288 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,6 +1,6 @@ import type { SerializedSession, Session, SessionContext, SessionStatus } from './types-hoist/session'; -import { uuid4 } from './utils-hoist/misc'; -import { timestampInSeconds } from './utils-hoist/time'; +import { uuid4 } from './utils/misc'; +import { timestampInSeconds } from './utils/time'; /** * Creates a new `Session` object by setting certain default parameters. If optional @param context diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 5f10f11db19c..adbcf0ae032a 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -9,14 +9,11 @@ import { } from '../semanticAttributes'; import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; +import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader } from '../utils/baggage'; +import { extractOrgIdFromDsnHost } from '../utils/dsn'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { addNonEnumerableProperty } from '../utils/object'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; -import { - baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, -} from '../utils-hoist/baggage'; -import { extractOrgIdFromDsnHost } from '../utils-hoist/dsn'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; import { getCapturedScopesOnSpan } from './utils'; /** diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index 34d2696da52d..28f582f32309 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -1,8 +1,8 @@ import { DEBUG_BUILD } from '../debug-build'; +import { addGlobalErrorInstrumentationHandler } from '../instrument/globalError'; +import { addGlobalUnhandledRejectionInstrumentationHandler } from '../instrument/globalUnhandledRejection'; +import { logger } from '../utils/logger'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; -import { addGlobalErrorInstrumentationHandler } from '../utils-hoist/instrument/globalError'; -import { addGlobalUnhandledRejectionInstrumentationHandler } from '../utils-hoist/instrument/globalUnhandledRejection'; -import { logger } from '../utils-hoist/logger'; import { SPAN_STATUS_ERROR } from './spanstatus'; let errorsInstrumented = false; diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index e5667d27a5ec..be0f1b80fb47 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -5,6 +5,7 @@ import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { logger } from '../utils/logger'; import { _setSpanForScope } from '../utils/spanOnScope'; import { getActiveSpan, @@ -13,8 +14,7 @@ import { spanTimeInputToSeconds, spanToJSON, } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; -import { timestampInSeconds } from '../utils-hoist/time'; +import { timestampInSeconds } from '../utils/time'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; import { SPAN_STATUS_ERROR } from './spanstatus'; diff --git a/packages/core/src/tracing/logSpans.ts b/packages/core/src/tracing/logSpans.ts index c0bcb34266ae..9728ac6f3399 100644 --- a/packages/core/src/tracing/logSpans.ts +++ b/packages/core/src/tracing/logSpans.ts @@ -1,7 +1,7 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Span } from '../types-hoist/span'; +import { logger } from '../utils/logger'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; /** * Print a log message for a started span. diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index b94cc6831009..5ee0397f8946 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -5,8 +5,8 @@ import { } from '../semanticAttributes'; import type { Measurements, MeasurementUnit } from '../types-hoist/measurement'; import type { TimedEvent } from '../types-hoist/timedEvent'; +import { logger } from '../utils/logger'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; /** * Adds a measurement to the active transaction on the current global scope. You can optionally pass in a different span diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 098a09baa4ad..8fec768c9d52 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -2,8 +2,8 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Options } from '../types-hoist/options'; import type { SamplingContext } from '../types-hoist/samplingcontext'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { logger } from '../utils/logger'; import { parseSampleRate } from '../utils/parseSampleRate'; -import { logger } from '../utils-hoist/logger'; /** * Makes a sampling decision for the given options. diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 9ace30c4e70a..69d1aa2a85ba 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -7,8 +7,8 @@ import type { SpanTimeInput, } from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; +import { generateSpanId, generateTraceId } from '../utils/propagationContext'; import { TRACE_FLAG_NONE } from '../utils/spanUtils'; -import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; /** * A Sentry Span that is non-recording, meaning it will not be sent to Sentry. diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index f0e2a6bb374a..74b59c655e83 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -25,6 +25,8 @@ import type { import type { SpanStatus } from '../types-hoist/spanStatus'; import type { TimedEvent } from '../types-hoist/timedEvent'; import type { TransactionSource } from '../types-hoist/transaction'; +import { logger } from '../utils/logger'; +import { generateSpanId, generateTraceId } from '../utils/propagationContext'; import { convertSpanLinksForEnvelope, getRootSpan, @@ -36,9 +38,7 @@ import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; -import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; -import { timestampInSeconds } from '../utils-hoist/time'; +import { timestampInSeconds } from '../utils/time'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanEnd } from './logSpans'; import { timedEventsToMeasurements } from './measurement'; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 427e4ebb0bf6..7a2229090e97 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -13,12 +13,12 @@ import type { SentrySpanArguments, Span, SpanTimeInput } from '../types-hoist/sp import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { logger } from '../utils/logger'; import { parseSampleRate } from '../utils/parseSampleRate'; +import { generateTraceId } from '../utils/propagationContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; -import { generateTraceId } from '../utils-hoist/propagationContext'; -import { propagationContextFromHeaders } from '../utils-hoist/tracing'; +import { propagationContextFromHeaders } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanStart } from './logSpans'; import { sampleSpan } from './sampling'; diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index f7e3d4924b35..5fa0a4b34420 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope'; import type { Span } from '../types-hoist/span'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; +import { addNonEnumerableProperty } from '../utils/object'; const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index d5a4f83c1657..9c74bb4261db 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -12,11 +12,11 @@ import { envelopeItemTypeToDataCategory, forEachEnvelopeItem, serializeEnvelope, -} from '../utils-hoist/envelope'; -import { logger } from '../utils-hoist/logger'; -import { type PromiseBuffer, makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from '../utils-hoist/promisebuffer'; -import { type RateLimits, isRateLimited, updateRateLimits } from '../utils-hoist/ratelimit'; -import { resolvedSyncPromise } from '../utils-hoist/syncpromise'; +} from '../utils/envelope'; +import { logger } from '../utils/logger'; +import { type PromiseBuffer, makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from '../utils/promisebuffer'; +import { type RateLimits, isRateLimited, updateRateLimits } from '../utils/ratelimit'; +import { resolvedSyncPromise } from '../utils/syncpromise'; export const DEFAULT_TRANSPORT_BUFFER_SIZE = 64; diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 8e93f5f81b53..1b6d53ab0ae8 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -2,8 +2,8 @@ import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api'; import type { Envelope, EnvelopeItemType, EventItem } from '../types-hoist/envelope'; import type { Event } from '../types-hoist/event'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; -import { dsnFromString } from '../utils-hoist/dsn'; -import { createEnvelope, forEachEnvelopeItem } from '../utils-hoist/envelope'; +import { dsnFromString } from '../utils/dsn'; +import { createEnvelope, forEachEnvelopeItem } from '../utils/envelope'; interface MatchParam { /** The envelope to be sent */ diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index b5e584039d1a..b7713b12bb2b 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -1,9 +1,9 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Envelope } from '../types-hoist/envelope'; import type { InternalBaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; -import { envelopeContainsItemType } from '../utils-hoist/envelope'; -import { logger } from '../utils-hoist/logger'; -import { parseRetryAfterHeader } from '../utils-hoist/ratelimit'; +import { envelopeContainsItemType } from '../utils/envelope'; +import { logger } from '../utils/logger'; +import { parseRetryAfterHeader } from '../utils/ratelimit'; export const MIN_DELAY = 100; // 100 ms export const START_DELAY = 5_000; // 5 seconds diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 7e29a69903a1..611dded043b6 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -2,8 +2,8 @@ import { getClient, withIsolationScope } from './currentScopes'; import { captureException } from './exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes'; import { startSpanManual } from './tracing'; -import { normalize } from './utils-hoist/normalize'; -import { addNonEnumerableProperty } from './utils-hoist/object'; +import { normalize } from './utils/normalize'; +import { addNonEnumerableProperty } from './utils/object'; interface SentryTrpcMiddlewareOptions { /** Whether to include procedure inputs in reported events. Defaults to `false`. */ diff --git a/packages/core/src/utils-hoist/aggregate-errors.ts b/packages/core/src/utils/aggregate-errors.ts similarity index 100% rename from packages/core/src/utils-hoist/aggregate-errors.ts rename to packages/core/src/utils/aggregate-errors.ts diff --git a/packages/core/src/utils-hoist/anr.ts b/packages/core/src/utils/anr.ts similarity index 100% rename from packages/core/src/utils-hoist/anr.ts rename to packages/core/src/utils/anr.ts diff --git a/packages/core/src/utils-hoist/baggage.ts b/packages/core/src/utils/baggage.ts similarity index 99% rename from packages/core/src/utils-hoist/baggage.ts rename to packages/core/src/utils/baggage.ts index 4a44ee5c35b0..0e90457db97b 100644 --- a/packages/core/src/utils-hoist/baggage.ts +++ b/packages/core/src/utils/baggage.ts @@ -1,5 +1,5 @@ +import { DEBUG_BUILD } from '../debug-build'; import type { DynamicSamplingContext } from '../types-hoist/envelope'; -import { DEBUG_BUILD } from './../debug-build'; import { isString } from './is'; import { logger } from './logger'; diff --git a/packages/core/src/utils-hoist/breadcrumb-log-level.ts b/packages/core/src/utils/breadcrumb-log-level.ts similarity index 100% rename from packages/core/src/utils-hoist/breadcrumb-log-level.ts rename to packages/core/src/utils/breadcrumb-log-level.ts diff --git a/packages/core/src/utils-hoist/browser.ts b/packages/core/src/utils/browser.ts similarity index 100% rename from packages/core/src/utils-hoist/browser.ts rename to packages/core/src/utils/browser.ts diff --git a/packages/core/src/utils-hoist/clientreport.ts b/packages/core/src/utils/clientreport.ts similarity index 100% rename from packages/core/src/utils-hoist/clientreport.ts rename to packages/core/src/utils/clientreport.ts diff --git a/packages/core/src/utils-hoist/debug-ids.ts b/packages/core/src/utils/debug-ids.ts similarity index 100% rename from packages/core/src/utils-hoist/debug-ids.ts rename to packages/core/src/utils/debug-ids.ts diff --git a/packages/core/src/utils-hoist/dsn.ts b/packages/core/src/utils/dsn.ts similarity index 98% rename from packages/core/src/utils-hoist/dsn.ts rename to packages/core/src/utils/dsn.ts index e26cbb4c8bf1..b22e2baed1f8 100644 --- a/packages/core/src/utils-hoist/dsn.ts +++ b/packages/core/src/utils/dsn.ts @@ -1,5 +1,5 @@ +import { DEBUG_BUILD } from '../debug-build'; import type { DsnComponents, DsnLike, DsnProtocol } from '../types-hoist/dsn'; -import { DEBUG_BUILD } from './../debug-build'; import { consoleSandbox, logger } from './logger'; /** Regular expression used to extract org ID from a DSN host. */ diff --git a/packages/core/src/utils-hoist/env.ts b/packages/core/src/utils/env.ts similarity index 100% rename from packages/core/src/utils-hoist/env.ts rename to packages/core/src/utils/env.ts diff --git a/packages/core/src/utils-hoist/envelope.ts b/packages/core/src/utils/envelope.ts similarity index 100% rename from packages/core/src/utils-hoist/envelope.ts rename to packages/core/src/utils/envelope.ts diff --git a/packages/core/src/utils-hoist/error.ts b/packages/core/src/utils/error.ts similarity index 100% rename from packages/core/src/utils-hoist/error.ts rename to packages/core/src/utils/error.ts diff --git a/packages/core/src/utils-hoist/eventbuilder.ts b/packages/core/src/utils/eventbuilder.ts similarity index 100% rename from packages/core/src/utils-hoist/eventbuilder.ts rename to packages/core/src/utils/eventbuilder.ts diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index 388613394148..a055e8875c06 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -2,8 +2,8 @@ import { getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { type Event } from '../types-hoist/event'; import { type Span } from '../types-hoist/span'; -import { logger } from '../utils-hoist/logger'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { logger } from '../utils/logger'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { getActiveSpan } from './spanUtils'; /** diff --git a/packages/core/src/utils/handleCallbackErrors.ts b/packages/core/src/utils/handleCallbackErrors.ts index 7e281b6b7d1c..cf4d29766445 100644 --- a/packages/core/src/utils/handleCallbackErrors.ts +++ b/packages/core/src/utils/handleCallbackErrors.ts @@ -1,4 +1,4 @@ -import { isThenable } from '../utils-hoist/is'; +import { isThenable } from '../utils/is'; /** * Wrap a callback function with error handling. diff --git a/packages/core/src/utils-hoist/is.ts b/packages/core/src/utils/is.ts similarity index 100% rename from packages/core/src/utils-hoist/is.ts rename to packages/core/src/utils/is.ts diff --git a/packages/core/src/utils-hoist/isBrowser.ts b/packages/core/src/utils/isBrowser.ts similarity index 100% rename from packages/core/src/utils-hoist/isBrowser.ts rename to packages/core/src/utils/isBrowser.ts diff --git a/packages/core/src/utils-hoist/logger.ts b/packages/core/src/utils/logger.ts similarity index 98% rename from packages/core/src/utils-hoist/logger.ts rename to packages/core/src/utils/logger.ts index 0c1e8f4d169b..669917dfbadf 100644 --- a/packages/core/src/utils-hoist/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -1,6 +1,6 @@ import { getGlobalSingleton } from '../carrier'; +import { DEBUG_BUILD } from '../debug-build'; import type { ConsoleLevel } from '../types-hoist/instrument'; -import { DEBUG_BUILD } from './../debug-build'; import { GLOBAL_OBJ } from './worldwide'; /** Prefix for logging strings */ diff --git a/packages/core/src/utils-hoist/lru.ts b/packages/core/src/utils/lru.ts similarity index 100% rename from packages/core/src/utils-hoist/lru.ts rename to packages/core/src/utils/lru.ts diff --git a/packages/core/src/utils-hoist/misc.ts b/packages/core/src/utils/misc.ts similarity index 100% rename from packages/core/src/utils-hoist/misc.ts rename to packages/core/src/utils/misc.ts diff --git a/packages/core/src/utils-hoist/node-stack-trace.ts b/packages/core/src/utils/node-stack-trace.ts similarity index 100% rename from packages/core/src/utils-hoist/node-stack-trace.ts rename to packages/core/src/utils/node-stack-trace.ts diff --git a/packages/core/src/utils-hoist/node.ts b/packages/core/src/utils/node.ts similarity index 100% rename from packages/core/src/utils-hoist/node.ts rename to packages/core/src/utils/node.ts diff --git a/packages/core/src/utils-hoist/normalize.ts b/packages/core/src/utils/normalize.ts similarity index 100% rename from packages/core/src/utils-hoist/normalize.ts rename to packages/core/src/utils/normalize.ts diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils/object.ts similarity index 99% rename from packages/core/src/utils-hoist/object.ts rename to packages/core/src/utils/object.ts index 366e3f2c5e98..c973ad056dee 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils/object.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { DEBUG_BUILD } from '../debug-build'; import type { WrappedFunction } from '../types-hoist/wrappedfunction'; -import { DEBUG_BUILD } from './../debug-build'; import { htmlTreeAsString } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPrimitive } from './is'; import { logger } from './logger'; diff --git a/packages/core/src/utils-hoist/path.ts b/packages/core/src/utils/path.ts similarity index 100% rename from packages/core/src/utils-hoist/path.ts rename to packages/core/src/utils/path.ts diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 30ed787d4879..6bff4e8b3d50 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -7,12 +7,12 @@ import { Scope } from '../scope'; import type { Event, EventHint } from '../types-hoist/event'; import type { ClientOptions } from '../types-hoist/options'; import type { StackParser } from '../types-hoist/stacktrace'; -import { getFilenameToDebugIdMap } from '../utils-hoist/debug-ids'; -import { addExceptionMechanism, uuid4 } from '../utils-hoist/misc'; -import { normalize } from '../utils-hoist/normalize'; -import { truncate } from '../utils-hoist/string'; -import { dateTimestampInSeconds } from '../utils-hoist/time'; import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; +import { getFilenameToDebugIdMap } from './debug-ids'; +import { addExceptionMechanism, uuid4 } from './misc'; +import { normalize } from './normalize'; +import { truncate } from './string'; +import { dateTimestampInSeconds } from './time'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. diff --git a/packages/core/src/utils-hoist/promisebuffer.ts b/packages/core/src/utils/promisebuffer.ts similarity index 100% rename from packages/core/src/utils-hoist/promisebuffer.ts rename to packages/core/src/utils/promisebuffer.ts diff --git a/packages/core/src/utils-hoist/propagationContext.ts b/packages/core/src/utils/propagationContext.ts similarity index 100% rename from packages/core/src/utils-hoist/propagationContext.ts rename to packages/core/src/utils/propagationContext.ts diff --git a/packages/core/src/utils-hoist/ratelimit.ts b/packages/core/src/utils/ratelimit.ts similarity index 100% rename from packages/core/src/utils-hoist/ratelimit.ts rename to packages/core/src/utils/ratelimit.ts diff --git a/packages/core/src/utils/sdkMetadata.ts b/packages/core/src/utils/sdkMetadata.ts index 9235efbe8c0c..437668a9e7f4 100644 --- a/packages/core/src/utils/sdkMetadata.ts +++ b/packages/core/src/utils/sdkMetadata.ts @@ -1,5 +1,5 @@ import type { Options } from '../types-hoist/options'; -import { SDK_VERSION } from '../utils-hoist/version'; +import { SDK_VERSION } from '../utils/version'; /** * A builder for the SDK metadata in the options for the SDK initialization. diff --git a/packages/core/src/utils-hoist/severity.ts b/packages/core/src/utils/severity.ts similarity index 100% rename from packages/core/src/utils-hoist/severity.ts rename to packages/core/src/utils/severity.ts diff --git a/packages/core/src/utils/spanOnScope.ts b/packages/core/src/utils/spanOnScope.ts index 29eec0a2574b..ad18e1687746 100644 --- a/packages/core/src/utils/spanOnScope.ts +++ b/packages/core/src/utils/spanOnScope.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope'; import type { Span } from '../types-hoist/span'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; +import { addNonEnumerableProperty } from '../utils/object'; const SCOPE_SPAN_FIELD = '_sentrySpan'; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 8018a62a20d4..361be95b6510 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -14,11 +14,11 @@ import type { TraceContext } from '../types-hoist/context'; import type { SpanLink, SpanLinkJSON } from '../types-hoist/link'; import type { Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; -import { consoleSandbox } from '../utils-hoist/logger'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; -import { generateSpanId } from '../utils-hoist/propagationContext'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { generateSentryTraceHeader } from '../utils-hoist/tracing'; +import { consoleSandbox } from '../utils/logger'; +import { addNonEnumerableProperty } from '../utils/object'; +import { generateSpanId } from '../utils/propagationContext'; +import { timestampInSeconds } from '../utils/time'; +import { generateSentryTraceHeader } from '../utils/tracing'; import { _getSpanForScope } from './spanOnScope'; // These are aligned with OpenTelemetry trace flags diff --git a/packages/core/src/utils-hoist/stacktrace.ts b/packages/core/src/utils/stacktrace.ts similarity index 100% rename from packages/core/src/utils-hoist/stacktrace.ts rename to packages/core/src/utils/stacktrace.ts diff --git a/packages/core/src/utils-hoist/string.ts b/packages/core/src/utils/string.ts similarity index 98% rename from packages/core/src/utils-hoist/string.ts rename to packages/core/src/utils/string.ts index 4771023fa2f5..ab98c794f681 100644 --- a/packages/core/src/utils-hoist/string.ts +++ b/packages/core/src/utils/string.ts @@ -1,6 +1,6 @@ import { isRegExp, isString, isVueViewModel } from './is'; -export { escapeStringForRegex } from './vendor/escapeStringForRegex'; +export { escapeStringForRegex } from '../vendor/escapeStringForRegex'; /** * Truncates given string to the maximum characters count diff --git a/packages/core/src/utils-hoist/supports.ts b/packages/core/src/utils/supports.ts similarity index 98% rename from packages/core/src/utils-hoist/supports.ts rename to packages/core/src/utils/supports.ts index 9cb6a71d8058..b5c96a7e50d8 100644 --- a/packages/core/src/utils-hoist/supports.ts +++ b/packages/core/src/utils/supports.ts @@ -1,4 +1,4 @@ -import { DEBUG_BUILD } from './../debug-build'; +import { DEBUG_BUILD } from '../debug-build'; import { logger } from './logger'; import { GLOBAL_OBJ } from './worldwide'; diff --git a/packages/core/src/utils-hoist/syncpromise.ts b/packages/core/src/utils/syncpromise.ts similarity index 100% rename from packages/core/src/utils-hoist/syncpromise.ts rename to packages/core/src/utils/syncpromise.ts diff --git a/packages/core/src/utils-hoist/time.ts b/packages/core/src/utils/time.ts similarity index 100% rename from packages/core/src/utils-hoist/time.ts rename to packages/core/src/utils/time.ts diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index ada865ed396d..e65271cf5a23 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -7,10 +7,10 @@ import type { Scope } from '../scope'; import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan } from '../tracing'; import type { Span } from '../types-hoist/span'; import type { SerializedTraceData } from '../types-hoist/tracing'; -import { dynamicSamplingContextToSentryBaggageHeader } from '../utils-hoist/baggage'; -import { logger } from '../utils-hoist/logger'; -import { generateSentryTraceHeader, TRACEPARENT_REGEXP } from '../utils-hoist/tracing'; +import { dynamicSamplingContextToSentryBaggageHeader } from './baggage'; +import { logger } from './logger'; import { getActiveSpan, spanToTraceHeader } from './spanUtils'; +import { generateSentryTraceHeader, TRACEPARENT_REGEXP } from './tracing'; /** * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation diff --git a/packages/core/src/utils-hoist/tracing.ts b/packages/core/src/utils/tracing.ts similarity index 98% rename from packages/core/src/utils-hoist/tracing.ts rename to packages/core/src/utils/tracing.ts index a3add28426f7..fe299d2d6338 100644 --- a/packages/core/src/utils-hoist/tracing.ts +++ b/packages/core/src/utils/tracing.ts @@ -1,8 +1,8 @@ import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { PropagationContext } from '../types-hoist/tracing'; import type { TraceparentData } from '../types-hoist/transaction'; -import { parseSampleRate } from '../utils/parseSampleRate'; import { baggageHeaderToDynamicSamplingContext } from './baggage'; +import { parseSampleRate } from './parseSampleRate'; import { generateSpanId, generateTraceId } from './propagationContext'; // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp is used for readability here diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils/url.ts similarity index 100% rename from packages/core/src/utils-hoist/url.ts rename to packages/core/src/utils/url.ts diff --git a/packages/core/src/utils-hoist/vercelWaitUntil.ts b/packages/core/src/utils/vercelWaitUntil.ts similarity index 100% rename from packages/core/src/utils-hoist/vercelWaitUntil.ts rename to packages/core/src/utils/vercelWaitUntil.ts diff --git a/packages/core/src/utils-hoist/version.ts b/packages/core/src/utils/version.ts similarity index 100% rename from packages/core/src/utils-hoist/version.ts rename to packages/core/src/utils/version.ts diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils/worldwide.ts similarity index 100% rename from packages/core/src/utils-hoist/worldwide.ts rename to packages/core/src/utils/worldwide.ts diff --git a/packages/core/src/utils-hoist/vendor/escapeStringForRegex.ts b/packages/core/src/vendor/escapeStringForRegex.ts similarity index 100% rename from packages/core/src/utils-hoist/vendor/escapeStringForRegex.ts rename to packages/core/src/vendor/escapeStringForRegex.ts diff --git a/packages/core/test/clear-global-scope.ts b/packages/core/test/clear-global-scope.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index 647095a98842..be7dd70dd068 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, test } from 'vitest'; import { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from '../../src/api'; import type { DsnComponents } from '../../src/types-hoist/dsn'; import type { SdkInfo } from '../../src/types-hoist/sdkinfo'; -import { makeDsn } from '../../src/utils-hoist/dsn'; +import { makeDsn } from '../../src/utils/dsn'; const ingestDsn = 'https://abc@xxxx.ingest.sentry.io:1234/subpath/123'; const dsnPublic = 'https://abc@sentry.io:1234/subpath/123'; diff --git a/packages/core/test/lib/attachments.test.ts b/packages/core/test/lib/attachments.test.ts index eab0927068fe..edab989af717 100644 --- a/packages/core/test/lib/attachments.test.ts +++ b/packages/core/test/lib/attachments.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { createTransport } from '../../src/transports/base'; -import { parseEnvelope } from '../../src/utils-hoist/envelope'; +import { parseEnvelope } from '../../src/utils/envelope'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; describe('Attachments', () => { diff --git a/packages/core/test/lib/carrier.test.ts b/packages/core/test/lib/carrier.test.ts index 144355dc1d3b..792121310a71 100644 --- a/packages/core/test/lib/carrier.test.ts +++ b/packages/core/test/lib/carrier.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { getSentryCarrier } from '../../src/carrier'; -import { SDK_VERSION } from '../../src/utils-hoist/version'; +import { SDK_VERSION } from '../../src/utils/version'; describe('getSentryCarrier', () => { describe('base case (one SDK)', () => { diff --git a/packages/core/test/lib/clear-global-scope.ts b/packages/core/test/lib/clear-global-scope.ts deleted file mode 100644 index 8fa0340a22bb..000000000000 --- a/packages/core/test/lib/clear-global-scope.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getSentryCarrier } from '../../src/carrier'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; - -export function clearGlobalScope() { - const carrier = getSentryCarrier(GLOBAL_OBJ); - carrier.globalScope = undefined; -} diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 07ce87fca3e2..844b49e4b55f 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -16,14 +16,14 @@ import * as integrationModule from '../../src/integration'; import type { Envelope } from '../../src/types-hoist/envelope'; import type { ErrorEvent, Event, TransactionEvent } from '../../src/types-hoist/event'; import type { SpanJSON } from '../../src/types-hoist/span'; -import * as loggerModule from '../../src/utils-hoist/logger'; -import * as miscModule from '../../src/utils-hoist/misc'; -import * as stringModule from '../../src/utils-hoist/string'; -import * as timeModule from '../../src/utils-hoist/time'; +import * as loggerModule from '../../src/utils/logger'; +import * as miscModule from '../../src/utils/misc'; +import * as stringModule from '../../src/utils/string'; +import * as timeModule from '../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { AdHocIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; -import { clearGlobalScope } from './clear-global-scope'; +import { clearGlobalScope } from '../testutils'; const PUBLIC_DSN = 'https://username@domain/123'; // eslint-disable-next-line no-var diff --git a/packages/core/test/utils-hoist/instrument/fetch.test.ts b/packages/core/test/lib/instrument/fetch.test.ts similarity index 93% rename from packages/core/test/utils-hoist/instrument/fetch.test.ts rename to packages/core/test/lib/instrument/fetch.test.ts index 0f08d9f03c6e..88d780a7dbad 100644 --- a/packages/core/test/utils-hoist/instrument/fetch.test.ts +++ b/packages/core/test/lib/instrument/fetch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseFetchArgs } from '../../../src/utils-hoist/instrument/fetch'; +import { parseFetchArgs } from '../../../src/instrument/fetch'; describe('instrument > parseFetchArgs', () => { it.each([ diff --git a/packages/core/test/utils-hoist/instrument.test.ts b/packages/core/test/lib/instrument/handlers.test.ts similarity index 81% rename from packages/core/test/utils-hoist/instrument.test.ts rename to packages/core/test/lib/instrument/handlers.test.ts index b6917994c96b..87e227a99323 100644 --- a/packages/core/test/utils-hoist/instrument.test.ts +++ b/packages/core/test/lib/instrument/handlers.test.ts @@ -1,5 +1,5 @@ import { describe, test } from 'vitest'; -import { maybeInstrument } from '../../src/utils-hoist/instrument/handlers'; +import { maybeInstrument } from '../../../src/instrument/handlers'; describe('maybeInstrument', () => { test('does not throw when instrumenting fails', () => { diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 3e4c26e0b959..58fabde15ef7 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -4,7 +4,7 @@ import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupInt import { setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types-hoist/integration'; import type { Options } from '../../src/types-hoist/options'; -import { logger } from '../../src/utils-hoist/logger'; +import { logger } from '../../src/utils/logger'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; function getTestClient(): TestClient { diff --git a/packages/core/test/lib/integrations/captureconsole.test.ts b/packages/core/test/lib/integrations/captureconsole.test.ts index 8c4e1e55ccce..faabc2590aac 100644 --- a/packages/core/test/lib/integrations/captureconsole.test.ts +++ b/packages/core/test/lib/integrations/captureconsole.test.ts @@ -4,13 +4,13 @@ import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vite import type { Client } from '../../../src'; import * as CurrentScopes from '../../../src/currentScopes'; import * as SentryCore from '../../../src/exports'; +import { addConsoleInstrumentationHandler } from '../../../src/instrument/console'; +import { resetInstrumentationHandlers } from '../../../src/instrument/handlers'; import { captureConsoleIntegration } from '../../../src/integrations/captureconsole'; import type { Event } from '../../../src/types-hoist/event'; import type { ConsoleLevel } from '../../../src/types-hoist/instrument'; -import { addConsoleInstrumentationHandler } from '../../../src/utils-hoist/instrument/console'; -import { resetInstrumentationHandlers } from '../../../src/utils-hoist/instrument/handlers'; -import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils-hoist/logger'; -import { GLOBAL_OBJ } from '../../../src/utils-hoist/worldwide'; +import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils/logger'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; const mockConsole: { [key in ConsoleLevel]: Mock } = { debug: vi.fn(), diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts index b9b705cc5cac..d68dbaeb5b56 100644 --- a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -3,9 +3,9 @@ import type { Client } from '../../../src/client'; import { thirdPartyErrorFilterIntegration } from '../../../src/integrations/third-party-errors-filter'; import { addMetadataToStackFrames } from '../../../src/metadata'; import type { Event } from '../../../src/types-hoist/event'; -import { nodeStackLineParser } from '../../../src/utils-hoist/node-stack-trace'; -import { createStackParser } from '../../../src/utils-hoist/stacktrace'; -import { GLOBAL_OBJ } from '../../../src/utils-hoist/worldwide'; +import { nodeStackLineParser } from '../../../src/utils/node-stack-trace'; +import { createStackParser } from '../../../src/utils/stacktrace'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; function clone(data: T): T { return JSON.parse(JSON.stringify(data)); diff --git a/packages/core/test/lib/logs/envelope.test.ts b/packages/core/test/lib/logs/envelope.test.ts index cd765cf018bc..7fbe1a439910 100644 --- a/packages/core/test/lib/logs/envelope.test.ts +++ b/packages/core/test/lib/logs/envelope.test.ts @@ -3,14 +3,14 @@ import { createLogContainerEnvelopeItem, createLogEnvelope } from '../../../src/ import type { DsnComponents } from '../../../src/types-hoist/dsn'; import type { SerializedLog } from '../../../src/types-hoist/log'; import type { SdkMetadata } from '../../../src/types-hoist/sdkmetadata'; -import * as utilsDsn from '../../../src/utils-hoist/dsn'; -import * as utilsEnvelope from '../../../src/utils-hoist/envelope'; +import * as utilsDsn from '../../../src/utils/dsn'; +import * as utilsEnvelope from '../../../src/utils/envelope'; -// Mock utils-hoist functions -vi.mock('../../../src/utils-hoist/dsn', () => ({ +// Mock utils functions +vi.mock('../../../src/utils/dsn', () => ({ dsnToString: vi.fn(dsn => `https://${dsn.publicKey}@${dsn.host}/`), })); -vi.mock('../../../src/utils-hoist/envelope', () => ({ +vi.mock('../../../src/utils/envelope', () => ({ createEnvelope: vi.fn((_headers, items) => [_headers, items]), })); diff --git a/packages/core/test/lib/logs/exports.test.ts b/packages/core/test/lib/logs/exports.test.ts index 3ba9f59b50d3..0dd73f064619 100644 --- a/packages/core/test/lib/logs/exports.test.ts +++ b/packages/core/test/lib/logs/exports.test.ts @@ -7,7 +7,7 @@ import { logAttributeToSerializedLogAttribute, } from '../../../src/logs/exports'; import type { Log } from '../../../src/types-hoist/log'; -import * as loggerModule from '../../../src/utils-hoist/logger'; +import * as loggerModule from '../../../src/utils/logger'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; diff --git a/packages/core/test/lib/metadata.test.ts b/packages/core/test/lib/metadata.test.ts index 87ffcc5284e0..bedf4cdcf7e9 100644 --- a/packages/core/test/lib/metadata.test.ts +++ b/packages/core/test/lib/metadata.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { addMetadataToStackFrames, getMetadataForUrl, stripMetadataFromStackFrames } from '../../src/metadata'; import type { Event } from '../../src/types-hoist/event'; -import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; -import { createStackParser } from '../../src/utils-hoist/stacktrace'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +import { nodeStackLineParser } from '../../src/utils/node-stack-trace'; +import { createStackParser } from '../../src/utils/stacktrace'; +import { GLOBAL_OBJ } from '../../src/utils/worldwide'; const parser = createStackParser(nodeStackLineParser()); diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 42279371dc30..d0fd86ae63f8 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -14,7 +14,7 @@ import { parseEventHintOrCaptureContext, prepareEvent, } from '../../src/utils/prepareEvent'; -import { clearGlobalScope } from './clear-global-scope'; +import { clearGlobalScope } from '../testutils'; describe('applyDebugIds', () => { afterEach(() => { diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index bac55994c199..280ba4c651ff 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -12,7 +12,7 @@ import type { Breadcrumb } from '../../src/types-hoist/breadcrumb'; import type { Event } from '../../src/types-hoist/event'; import { applyScopeDataToEvent } from '../../src/utils/applyScopeDataToEvent'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; -import { clearGlobalScope } from './clear-global-scope'; +import { clearGlobalScope } from '../testutils'; describe('Scope', () => { beforeEach(() => { diff --git a/packages/core/test/lib/session.test.ts b/packages/core/test/lib/session.test.ts index 6744b2107e7b..9d6cfe06e426 100644 --- a/packages/core/test/lib/session.test.ts +++ b/packages/core/test/lib/session.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, test } from 'vitest'; import { closeSession, makeSession, updateSession } from '../../src/session'; import type { SessionContext } from '../../src/types-hoist/session'; -import { timestampInSeconds } from '../../src/utils-hoist/time'; +import { timestampInSeconds } from '../../src/utils/time'; describe('Session', () => { it('initializes with the proper defaults', () => { diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index cadc8b79219b..128ee9180d54 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { setCurrentClient, spanToJSON, startInactiveSpan, startSpan } from '../../../src'; +import * as globalErrorModule from '../../../src/instrument/globalError'; +import * as globalUnhandledRejectionModule from '../../../src/instrument/globalUnhandledRejection'; import { _resetErrorsInstrumented, registerSpanErrorInstrumentation } from '../../../src/tracing/errors'; import type { HandlerDataError, HandlerDataUnhandledRejection } from '../../../src/types-hoist/instrument'; -import * as globalErrorModule from '../../../src/utils-hoist/instrument/globalError'; -import * as globalUnhandledRejectionModule from '../../../src/utils-hoist/instrument/globalUnhandledRejection'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; let mockErrorCallback: (data: HandlerDataError) => void = () => {}; diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index a63e161e5ada..601e25be0d23 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -6,7 +6,7 @@ import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; import type { SpanJSON } from '../../../src/types-hoist/span'; import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; -import { timestampInSeconds } from '../../../src/utils-hoist/time'; +import { timestampInSeconds } from '../../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; describe('SentrySpan', () => { diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index af3655d89972..ef2220ac1f8b 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it, vi } from 'vitest'; import { createTransport } from '../../../src/transports/base'; import type { AttachmentItem, EventEnvelope, EventItem } from '../../../src/types-hoist/envelope'; import type { TransportMakeRequestResponse } from '../../../src/types-hoist/transport'; -import { createEnvelope, serializeEnvelope } from '../../../src/utils-hoist/envelope'; -import type { PromiseBuffer } from '../../../src/utils-hoist/promisebuffer'; -import { resolvedSyncPromise } from '../../../src/utils-hoist/syncpromise'; +import { createEnvelope, serializeEnvelope } from '../../../src/utils/envelope'; +import type { PromiseBuffer } from '../../../src/utils/promisebuffer'; +import { resolvedSyncPromise } from '../../../src/utils/syncpromise'; const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/core/test/utils-hoist/aggregate-errors.test.ts b/packages/core/test/lib/utils/aggregate-errors.test.ts similarity index 95% rename from packages/core/test/utils-hoist/aggregate-errors.test.ts rename to packages/core/test/lib/utils/aggregate-errors.test.ts index 007d81678082..01ede6d9186a 100644 --- a/packages/core/test/utils-hoist/aggregate-errors.test.ts +++ b/packages/core/test/lib/utils/aggregate-errors.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from 'vitest'; -import type { ExtendedError } from '../../src/types-hoist/error'; -import type { Event, EventHint } from '../../src/types-hoist/event'; -import type { Exception } from '../../src/types-hoist/exception'; -import type { StackParser } from '../../src/types-hoist/stacktrace'; -import { applyAggregateErrorsToEvent } from '../../src/utils-hoist/aggregate-errors'; -import { createStackParser } from '../../src/utils-hoist/stacktrace'; +import type { ExtendedError } from '../../../src/types-hoist/error'; +import type { Event, EventHint } from '../../../src/types-hoist/event'; +import type { Exception } from '../../../src/types-hoist/exception'; +import type { StackParser } from '../../../src/types-hoist/stacktrace'; +import { applyAggregateErrorsToEvent } from '../../../src/utils/aggregate-errors'; +import { createStackParser } from '../../../src/utils/stacktrace'; const stackParser = createStackParser([0, line => ({ filename: line })]); const exceptionFromError = (_stackParser: StackParser, ex: Error): Exception => { diff --git a/packages/core/test/utils-hoist/baggage.test.ts b/packages/core/test/lib/utils/baggage.test.ts similarity index 98% rename from packages/core/test/utils-hoist/baggage.test.ts rename to packages/core/test/lib/utils/baggage.test.ts index c05ac0d5dd96..4816a3fbf079 100644 --- a/packages/core/test/utils-hoist/baggage.test.ts +++ b/packages/core/test/lib/utils/baggage.test.ts @@ -3,7 +3,7 @@ import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader, parseBaggageHeader, -} from '../../src/utils-hoist/baggage'; +} from '../../../src/utils/baggage'; test.each([ ['', undefined], diff --git a/packages/core/test/utils-hoist/breadcrumb-log-level.test.ts b/packages/core/test/lib/utils/breadcrumb-log-level.test.ts similarity index 82% rename from packages/core/test/utils-hoist/breadcrumb-log-level.test.ts rename to packages/core/test/lib/utils/breadcrumb-log-level.test.ts index 86062e025f37..506567c72b8b 100644 --- a/packages/core/test/utils-hoist/breadcrumb-log-level.test.ts +++ b/packages/core/test/lib/utils/breadcrumb-log-level.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getBreadcrumbLogLevelFromHttpStatusCode } from '../../src/utils-hoist/breadcrumb-log-level'; +import { getBreadcrumbLogLevelFromHttpStatusCode } from '../../../src/utils/breadcrumb-log-level'; describe('getBreadcrumbLogLevelFromHttpStatusCode()', () => { it.each([ diff --git a/packages/core/test/utils-hoist/browser.test.ts b/packages/core/test/lib/utils/browser.test.ts similarity index 97% rename from packages/core/test/utils-hoist/browser.test.ts rename to packages/core/test/lib/utils/browser.test.ts index d7fc8aab022c..0bcf71884482 100644 --- a/packages/core/test/utils-hoist/browser.test.ts +++ b/packages/core/test/lib/utils/browser.test.ts @@ -1,6 +1,6 @@ import { JSDOM } from 'jsdom'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { htmlTreeAsString } from '../../src/utils-hoist/browser'; +import { htmlTreeAsString } from '../../../src/utils/browser'; beforeAll(() => { const dom = new JSDOM(); diff --git a/packages/core/test/utils-hoist/clientreport.test.ts b/packages/core/test/lib/utils/clientreport.test.ts similarity index 88% rename from packages/core/test/utils-hoist/clientreport.test.ts rename to packages/core/test/lib/utils/clientreport.test.ts index 49f6a67b35cb..8acc9c9d8dc0 100644 --- a/packages/core/test/utils-hoist/clientreport.test.ts +++ b/packages/core/test/lib/utils/clientreport.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { ClientReport } from '../../src/types-hoist/clientreport'; -import { createClientReportEnvelope } from '../../src/utils-hoist/clientreport'; -import { parseEnvelope, serializeEnvelope } from '../../src/utils-hoist/envelope'; +import type { ClientReport } from '../../../src/types-hoist/clientreport'; +import { createClientReportEnvelope } from '../../../src/utils/clientreport'; +import { parseEnvelope, serializeEnvelope } from '../../../src/utils/envelope'; const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [ { diff --git a/packages/core/test/utils-hoist/dsn.test.ts b/packages/core/test/lib/utils/dsn.test.ts similarity index 98% rename from packages/core/test/utils-hoist/dsn.test.ts rename to packages/core/test/lib/utils/dsn.test.ts index b5d22130816b..3dc081866703 100644 --- a/packages/core/test/utils-hoist/dsn.test.ts +++ b/packages/core/test/lib/utils/dsn.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, test, vi } from 'vitest'; -import { DEBUG_BUILD } from '../../src/debug-build'; -import { dsnToString, extractOrgIdFromDsnHost, makeDsn } from '../../src/utils-hoist/dsn'; -import { logger } from '../../src/utils-hoist/logger'; +import { DEBUG_BUILD } from '../../../src/debug-build'; +import { dsnToString, extractOrgIdFromDsnHost, makeDsn } from '../../../src/utils/dsn'; +import { logger } from '../../../src/utils/logger'; function testIf(condition: boolean) { return condition ? test : test.skip; diff --git a/packages/core/test/utils-hoist/envelope.test.ts b/packages/core/test/lib/utils/envelope.test.ts similarity index 94% rename from packages/core/test/utils-hoist/envelope.test.ts rename to packages/core/test/lib/utils/envelope.test.ts index 00a745807fcd..85bff6a662f2 100644 --- a/packages/core/test/utils-hoist/envelope.test.ts +++ b/packages/core/test/lib/utils/envelope.test.ts @@ -6,10 +6,10 @@ import { spanToJSON, } from '@sentry/core'; import { afterEach, describe, expect, it, test, vi } from 'vitest'; -import { getSentryCarrier } from '../../src/carrier'; -import type { EventEnvelope } from '../../src/types-hoist/envelope'; -import type { Event } from '../../src/types-hoist/event'; -import type { SpanAttributes } from '../../src/types-hoist/span'; +import { getSentryCarrier } from '../../../src/carrier'; +import type { EventEnvelope } from '../../../src/types-hoist/envelope'; +import type { Event } from '../../../src/types-hoist/event'; +import type { SpanAttributes } from '../../../src/types-hoist/span'; import { addItemToEnvelope, createEnvelope, @@ -17,9 +17,9 @@ import { forEachEnvelopeItem, parseEnvelope, serializeEnvelope, -} from '../../src/utils-hoist/envelope'; -import type { InternalGlobal } from '../../src/utils-hoist/worldwide'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +} from '../../../src/utils/envelope'; +import type { InternalGlobal } from '../../../src/utils/worldwide'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; describe('envelope', () => { describe('createSpanEnvelope()', () => { diff --git a/packages/core/test/utils-hoist/eventbuilder.test.ts b/packages/core/test/lib/utils/eventbuilder.test.ts similarity index 95% rename from packages/core/test/utils-hoist/eventbuilder.test.ts rename to packages/core/test/lib/utils/eventbuilder.test.ts index 877df25d8fed..77fa2ff93d96 100644 --- a/packages/core/test/utils-hoist/eventbuilder.test.ts +++ b/packages/core/test/lib/utils/eventbuilder.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, test } from 'vitest'; -import type { Client } from '../../src/client'; -import { eventFromMessage, eventFromUnknownInput } from '../../src/utils-hoist/eventbuilder'; -import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; -import { createStackParser } from '../../src/utils-hoist/stacktrace'; +import type { Client } from '../../../src/client'; +import { eventFromMessage, eventFromUnknownInput } from '../../../src/utils/eventbuilder'; +import { nodeStackLineParser } from '../../../src/utils/node-stack-trace'; +import { createStackParser } from '../../../src/utils/stacktrace'; const stackParser = createStackParser(nodeStackLineParser()); diff --git a/packages/core/test/lib/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts index 14258c2caf36..8f6e512bca7b 100644 --- a/packages/core/test/lib/utils/featureFlags.test.ts +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -5,7 +5,7 @@ import { _INTERNAL_insertFlagToScope, _INTERNAL_insertToFlagBuffer, } from '../../../src/utils/featureFlags'; -import { logger } from '../../../src/utils-hoist/logger'; +import { logger } from '../../../src/utils/logger'; describe('flags', () => { describe('insertFlagToScope()', () => { diff --git a/packages/core/test/utils-hoist/is.test.ts b/packages/core/test/lib/utils/is.test.ts similarity index 95% rename from packages/core/test/utils-hoist/is.test.ts rename to packages/core/test/lib/utils/is.test.ts index 70a83eee5efd..745cf275be06 100644 --- a/packages/core/test/utils-hoist/is.test.ts +++ b/packages/core/test/lib/utils/is.test.ts @@ -9,10 +9,10 @@ import { isPrimitive, isThenable, isVueViewModel, -} from '../../src/utils-hoist/is'; -import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../../src/utils-hoist/supports'; -import { resolvedSyncPromise } from '../../src/utils-hoist/syncpromise'; -import { testOnlyIfNodeVersionAtLeast } from './testutils'; +} from '../../../src/utils/is'; +import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../../../src/utils/supports'; +import { resolvedSyncPromise } from '../../../src/utils/syncpromise'; +import { testOnlyIfNodeVersionAtLeast } from '../../testutils'; if (supportsDOMError()) { describe('isDOMError()', () => { diff --git a/packages/core/test/utils-hoist/lru.test.ts b/packages/core/test/lib/utils/lru.test.ts similarity index 94% rename from packages/core/test/utils-hoist/lru.test.ts rename to packages/core/test/lib/utils/lru.test.ts index 28259a2700ad..5940f10684e1 100644 --- a/packages/core/test/utils-hoist/lru.test.ts +++ b/packages/core/test/lib/utils/lru.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { LRUMap } from '../../src/utils-hoist/lru'; +import { LRUMap } from '../../../src/utils/lru'; describe('LRUMap', () => { test('evicts older entries when reaching max size', () => { diff --git a/packages/core/test/utils-hoist/misc.test.ts b/packages/core/test/lib/utils/misc.test.ts similarity index 97% rename from packages/core/test/utils-hoist/misc.test.ts rename to packages/core/test/lib/utils/misc.test.ts index 6aece21ecb81..83e7f4c05b66 100644 --- a/packages/core/test/utils-hoist/misc.test.ts +++ b/packages/core/test/lib/utils/misc.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it, test } from 'vitest'; -import type { Event } from '../../src/types-hoist/event'; -import type { Mechanism } from '../../src/types-hoist/mechanism'; -import type { StackFrame } from '../../src/types-hoist/stackframe'; +import type { Event } from '../../../src/types-hoist/event'; +import type { Mechanism } from '../../../src/types-hoist/mechanism'; +import type { StackFrame } from '../../../src/types-hoist/stackframe'; import { addContextToFrame, addExceptionMechanism, checkOrSetAlreadyCaught, getEventDescription, uuid4, -} from '../../src/utils-hoist/misc'; +} from '../../../src/utils/misc'; describe('getEventDescription()', () => { test('message event', () => { diff --git a/packages/core/test/utils-hoist/normalize-url.test.ts b/packages/core/test/lib/utils/normalize-url.test.ts similarity index 97% rename from packages/core/test/utils-hoist/normalize-url.test.ts rename to packages/core/test/lib/utils/normalize-url.test.ts index 0e8b5d787601..888383940a1a 100644 --- a/packages/core/test/utils-hoist/normalize-url.test.ts +++ b/packages/core/test/lib/utils/normalize-url.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { normalizeUrlToBase } from '../../src/utils-hoist/normalize'; +import { normalizeUrlToBase } from '../../../src/utils/normalize'; describe('normalizeUrlToBase()', () => { it('Example app on Windows', () => { diff --git a/packages/core/test/utils-hoist/normalize.test.ts b/packages/core/test/lib/utils/normalize.test.ts similarity index 99% rename from packages/core/test/utils-hoist/normalize.test.ts rename to packages/core/test/lib/utils/normalize.test.ts index e08c7187c839..17c0628e53df 100644 --- a/packages/core/test/utils-hoist/normalize.test.ts +++ b/packages/core/test/lib/utils/normalize.test.ts @@ -3,9 +3,9 @@ */ import { describe, expect, test, vi } from 'vitest'; -import { addNonEnumerableProperty, normalize } from '../../src'; -import * as isModule from '../../src/utils-hoist/is'; -import * as stacktraceModule from '../../src/utils-hoist/stacktrace'; +import { addNonEnumerableProperty, normalize } from '../../../src'; +import * as isModule from '../../../src/utils/is'; +import * as stacktraceModule from '../../../src/utils/stacktrace'; describe('normalize()', () => { describe('acts as a pass-through for simple-cases', () => { diff --git a/packages/core/test/utils-hoist/object.test.ts b/packages/core/test/lib/utils/object.test.ts similarity index 98% rename from packages/core/test/utils-hoist/object.test.ts rename to packages/core/test/lib/utils/object.test.ts index c6c5d05b6af0..bc8c7611abb8 100644 --- a/packages/core/test/utils-hoist/object.test.ts +++ b/packages/core/test/lib/utils/object.test.ts @@ -3,7 +3,7 @@ */ import { describe, expect, it, test, vi } from 'vitest'; -import type { WrappedFunction } from '../../src/types-hoist/wrappedfunction'; +import type { WrappedFunction } from '../../../src/types-hoist/wrappedfunction'; import { addNonEnumerableProperty, dropUndefinedKeys, @@ -11,8 +11,8 @@ import { fill, markFunctionWrapped, objectify, -} from '../../src/utils-hoist/object'; -import { testOnlyIfNodeVersionAtLeast } from './testutils'; +} from '../../../src/utils/object'; +import { testOnlyIfNodeVersionAtLeast } from '../../testutils'; describe('fill()', () => { test('wraps a method by calling a replacement function on it', () => { diff --git a/packages/core/test/utils-hoist/path.test.ts b/packages/core/test/lib/utils/path.test.ts similarity index 95% rename from packages/core/test/utils-hoist/path.test.ts rename to packages/core/test/lib/utils/path.test.ts index 6cd760ac84aa..7e17f99228d4 100644 --- a/packages/core/test/utils-hoist/path.test.ts +++ b/packages/core/test/lib/utils/path.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { basename, dirname } from '../../src/utils-hoist/path'; +import { basename, dirname } from '../../../src/utils/path'; describe('path', () => { describe('basename', () => { diff --git a/packages/core/test/utils-hoist/promisebuffer.test.ts b/packages/core/test/lib/utils/promisebuffer.test.ts similarity index 96% rename from packages/core/test/utils-hoist/promisebuffer.test.ts rename to packages/core/test/lib/utils/promisebuffer.test.ts index 6b0b81d1588e..1f0011dd6e50 100644 --- a/packages/core/test/utils-hoist/promisebuffer.test.ts +++ b/packages/core/test/lib/utils/promisebuffer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi } from 'vitest'; -import { makePromiseBuffer } from '../../src/utils-hoist/promisebuffer'; -import { SyncPromise } from '../../src/utils-hoist/syncpromise'; +import { makePromiseBuffer } from '../../../src/utils/promisebuffer'; +import { SyncPromise } from '../../../src/utils/syncpromise'; describe('PromiseBuffer', () => { describe('add()', () => { diff --git a/packages/core/test/utils-hoist/ratelimit.test.ts b/packages/core/test/lib/utils/ratelimit.test.ts similarity index 98% rename from packages/core/test/utils-hoist/ratelimit.test.ts rename to packages/core/test/lib/utils/ratelimit.test.ts index 556280b93ba4..772bd5e5c9d4 100644 --- a/packages/core/test/utils-hoist/ratelimit.test.ts +++ b/packages/core/test/lib/utils/ratelimit.test.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'vitest'; -import type { RateLimits } from '../../src/utils-hoist/ratelimit'; +import type { RateLimits } from '../../../src/utils/ratelimit'; import { DEFAULT_RETRY_AFTER, disabledUntil, isRateLimited, parseRetryAfterHeader, updateRateLimits, -} from '../../src/utils-hoist/ratelimit'; +} from '../../../src/utils/ratelimit'; describe('parseRetryAfterHeader()', () => { test('should fallback to 60s when incorrect header provided', () => { diff --git a/packages/core/test/utils-hoist/severity.test.ts b/packages/core/test/lib/utils/severity.test.ts similarity index 87% rename from packages/core/test/utils-hoist/severity.test.ts rename to packages/core/test/lib/utils/severity.test.ts index 2f5595dfc9f0..6cc44a5bbeee 100644 --- a/packages/core/test/utils-hoist/severity.test.ts +++ b/packages/core/test/lib/utils/severity.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { severityLevelFromString } from '../../src/utils-hoist/severity'; +import { severityLevelFromString } from '../../../src/utils/severity'; describe('severityLevelFromString()', () => { test("converts 'warn' to 'warning'", () => { diff --git a/packages/core/test/utils-hoist/stacktrace.test.ts b/packages/core/test/lib/utils/stacktrace.test.ts similarity index 98% rename from packages/core/test/utils-hoist/stacktrace.test.ts rename to packages/core/test/lib/utils/stacktrace.test.ts index cfcc70819fa0..b0d74e2e9f75 100644 --- a/packages/core/test/utils-hoist/stacktrace.test.ts +++ b/packages/core/test/lib/utils/stacktrace.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; -import { stripSentryFramesAndReverse } from '../../src/utils-hoist/stacktrace'; +import { nodeStackLineParser } from '../../../src/utils/node-stack-trace'; +import { stripSentryFramesAndReverse } from '../../../src/utils/stacktrace'; describe('Stacktrace', () => { describe('stripSentryFramesAndReverse()', () => { diff --git a/packages/core/test/utils-hoist/string.test.ts b/packages/core/test/lib/utils/string.test.ts similarity index 99% rename from packages/core/test/utils-hoist/string.test.ts rename to packages/core/test/lib/utils/string.test.ts index 364d29223a66..b3d166568163 100644 --- a/packages/core/test/utils-hoist/string.test.ts +++ b/packages/core/test/lib/utils/string.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { isMatchingPattern, stringMatchesSomePattern, truncate } from '../../src/utils-hoist/string'; +import { isMatchingPattern, stringMatchesSomePattern, truncate } from '../../../src/utils/string'; describe('truncate()', () => { test('it works as expected', () => { diff --git a/packages/core/test/utils-hoist/supports.test.ts b/packages/core/test/lib/utils/supports.test.ts similarity index 93% rename from packages/core/test/utils-hoist/supports.test.ts rename to packages/core/test/lib/utils/supports.test.ts index fffc6be02e1e..97ad75d6c7c3 100644 --- a/packages/core/test/utils-hoist/supports.test.ts +++ b/packages/core/test/lib/utils/supports.test.ts @@ -1,6 +1,6 @@ import { afterEach } from 'node:test'; import { describe, expect, it } from 'vitest'; -import { supportsHistory } from '../../src/utils-hoist/supports'; +import { supportsHistory } from '../../../src/utils/supports'; describe('supportsHistory', () => { const originalHistory = globalThis.history; diff --git a/packages/core/test/utils-hoist/syncpromise.test.ts b/packages/core/test/lib/utils/syncpromise.test.ts similarity index 99% rename from packages/core/test/utils-hoist/syncpromise.test.ts rename to packages/core/test/lib/utils/syncpromise.test.ts index e332de2c2141..14bc5d7abbe0 100644 --- a/packages/core/test/utils-hoist/syncpromise.test.ts +++ b/packages/core/test/lib/utils/syncpromise.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from 'vitest'; -import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from '../../src/utils-hoist/syncpromise'; +import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from '../../../src/utils/syncpromise'; describe('SyncPromise', () => { test('simple', async () => { diff --git a/packages/core/test/utils-hoist/tracing.test.ts b/packages/core/test/lib/utils/tracing.test.ts similarity index 99% rename from packages/core/test/utils-hoist/tracing.test.ts rename to packages/core/test/lib/utils/tracing.test.ts index 851ee7b109c4..ea99555e70e1 100644 --- a/packages/core/test/utils-hoist/tracing.test.ts +++ b/packages/core/test/lib/utils/tracing.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, test } from 'vitest'; -import { extractTraceparentData, propagationContextFromHeaders } from '../../src/utils-hoist/tracing'; +import { extractTraceparentData, propagationContextFromHeaders } from '../../../src/utils/tracing'; const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; const EXAMPLE_BAGGAGE = 'sentry-release=1.2.3,sentry-foo=bar,other=baz,sentry-sample_rand=0.42'; diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/lib/utils/url.test.ts similarity index 99% rename from packages/core/test/utils-hoist/url.test.ts rename to packages/core/test/lib/utils/url.test.ts index 67ec8b31644f..33364d66daa5 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/lib/utils/url.test.ts @@ -7,7 +7,7 @@ import { parseStringToURLObject, parseUrl, stripUrlQueryAndFragment, -} from '../../src/utils-hoist/url'; +} from '../../../src/utils/url'; describe('stripQueryStringAndFragment', () => { const urlString = 'http://dogs.are.great:1231/yay/'; diff --git a/packages/core/test/utils-hoist/vercelWaitUntil.test.ts b/packages/core/test/lib/utils/vercelWaitUntil.test.ts similarity index 90% rename from packages/core/test/utils-hoist/vercelWaitUntil.test.ts rename to packages/core/test/lib/utils/vercelWaitUntil.test.ts index c35b5f076cd4..78637cb3ef18 100644 --- a/packages/core/test/utils-hoist/vercelWaitUntil.test.ts +++ b/packages/core/test/lib/utils/vercelWaitUntil.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { vercelWaitUntil } from '../../src/utils-hoist/vercelWaitUntil'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +import { vercelWaitUntil } from '../../../src/utils/vercelWaitUntil'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; describe('vercelWaitUntil', () => { it('should do nothing if GLOBAL_OBJ does not have the @vercel/request-context symbol', () => { diff --git a/packages/core/test/utils-hoist/worldwide.test.ts b/packages/core/test/lib/utils/worldwide.test.ts similarity index 85% rename from packages/core/test/utils-hoist/worldwide.test.ts rename to packages/core/test/lib/utils/worldwide.test.ts index efd8a152a0ab..f7c952a9930d 100644 --- a/packages/core/test/utils-hoist/worldwide.test.ts +++ b/packages/core/test/lib/utils/worldwide.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; describe('GLOBAL_OBJ', () => { test('should return the same object', () => { diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 811046972675..2b7162a112b5 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -8,7 +8,7 @@ import type { ClientOptions } from '../../src/types-hoist/options'; import type { ParameterizedString } from '../../src/types-hoist/parameterize'; import type { Session } from '../../src/types-hoist/session'; import type { SeverityLevel } from '../../src/types-hoist/severity'; -import { resolvedSyncPromise } from '../../src/utils-hoist/syncpromise'; +import { resolvedSyncPromise } from '../../src/utils/syncpromise'; export function getDefaultTestClientOptions(options: Partial = {}): TestClientOptions { return { diff --git a/packages/core/test/mocks/transport.ts b/packages/core/test/mocks/transport.ts index ba49025ce5d7..c8ae84f80d64 100644 --- a/packages/core/test/mocks/transport.ts +++ b/packages/core/test/mocks/transport.ts @@ -1,6 +1,6 @@ import { createTransport } from '../../src/transports/base'; import type { Transport } from '../../src/types-hoist/transport'; -import { SyncPromise } from '../../src/utils-hoist/syncpromise'; +import { SyncPromise } from '../../src/utils/syncpromise'; async function sleep(delay: number): Promise { return new SyncPromise(resolve => setTimeout(resolve, delay)); diff --git a/packages/core/test/utils-hoist/testutils.ts b/packages/core/test/testutils.ts similarity index 65% rename from packages/core/test/utils-hoist/testutils.ts rename to packages/core/test/testutils.ts index b830095d3393..c00ec2b878b7 100644 --- a/packages/core/test/utils-hoist/testutils.ts +++ b/packages/core/test/testutils.ts @@ -1,4 +1,6 @@ import { it } from 'vitest'; +import { getSentryCarrier } from '../src/carrier'; +import { GLOBAL_OBJ } from '../src/utils/worldwide'; // eslint-disable-next-line @typescript-eslint/ban-types export const testOnlyIfNodeVersionAtLeast = (minVersion: number): Function => { @@ -14,3 +16,8 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): Function => { return it; }; + +export function clearGlobalScope() { + const carrier = getSentryCarrier(GLOBAL_OBJ); + carrier.globalScope = undefined; +} diff --git a/packages/core/test/utils-hoist/types/typedef.test.ts b/packages/core/test/types/typedef.test.ts similarity index 100% rename from packages/core/test/utils-hoist/types/typedef.test.ts rename to packages/core/test/types/typedef.test.ts diff --git a/packages/node/test/integration/scope.test.ts b/packages/node/test/integration/scope.test.ts index 43af4214980d..6f2acaf267ee 100644 --- a/packages/node/test/integration/scope.test.ts +++ b/packages/node/test/integration/scope.test.ts @@ -1,7 +1,6 @@ import { getCapturedScopesOnSpan, getCurrentScope } from '@sentry/core'; import { getClient } from '@sentry/opentelemetry'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { clearGlobalScope } from '../../../core/test/lib/clear-global-scope'; import * as Sentry from '../../src/'; import type { NodeClient } from '../../src/sdk/client'; import { cleanupOtel, mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; @@ -240,7 +239,7 @@ describe('Integration | Scope', () => { describe('global scope', () => { beforeEach(() => { - clearGlobalScope(); + resetGlobals(); }); it('works before calling init', () => { From 57256ad3eb6582f772ded7b938efe4a051253d9e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 18 Jun 2025 16:36:13 +0200 Subject: [PATCH 13/25] feat(nextjs): Add option for auto-generated random tunnel route (#16626) This PR adds support for auto-generated tunnel paths to avoid ad-blocker detection. - Random paths are less likely to be blocked than `/monitoring` - Users can just set `tunnelRoute: true` for an auto-generated route - Existing configs will still work **Note that every build will now create a different random path which should be quite unpredictable for ad-blockers** --- packages/nextjs/src/config/types.ts | 5 ++- packages/nextjs/src/config/webpack.ts | 4 +- .../nextjs/src/config/withSentryConfig.ts | 23 +++++++++++- .../nextjs/test/utils/tunnelRoute.test.ts | 37 +++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index ffce8a8b0641..fe05624ba15e 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -426,9 +426,12 @@ export type SentryBuildOptions = { * Tunnel Sentry requests through this route on the Next.js server, to circumvent ad-blockers blocking Sentry events * from being sent. This option should be a path (for example: '/error-monitoring'). * + * - Pass `true` to auto-generate a random, ad-blocker-resistant route for each build + * - Pass a string path (e.g., '/monitoring') to use a custom route + * * NOTE: This feature only works with Next.js 11+ */ - tunnelRoute?: string; + tunnelRoute?: string | boolean; /** * Tree shakes Sentry SDK logger statements from the bundle. diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8898b3495ba9..aeef52d4e309 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -692,7 +692,9 @@ function addValueInjectionLoader( const isomorphicValues = { // `rewritesTunnel` set by the user in Next.js config _sentryRewritesTunnelPath: - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' ? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}` : undefined, diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index b185cc3d3211..ac1e76231eb8 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -75,6 +75,15 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu } } +/** + * Generates a random tunnel route path that's less likely to be blocked by ad-blockers + */ +function generateRandomTunnelRoute(): string { + // Generate a random 8-character alphanumeric string + const randomString = Math.random().toString(36).substring(2, 10); + return `/${randomString}`; +} + // Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the // `webpack` property function getFinalConfigObject( @@ -93,7 +102,14 @@ function getFinalConfigObject( ); } } else { - setUpTunnelRewriteRules(incomingUserNextConfigObject, userSentryOptions.tunnelRoute); + const resolvedTunnelRoute = + typeof userSentryOptions.tunnelRoute === 'boolean' + ? generateRandomTunnelRoute() + : userSentryOptions.tunnelRoute; + + // Update the global options object to use the resolved value everywhere + userSentryOptions.tunnelRoute = resolvedTunnelRoute; + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -363,8 +379,11 @@ function setUpBuildTimeVariables( ): void { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const basePath = userNextConfig.basePath ?? ''; + const rewritesTunnelPath = - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' ? `${basePath}${userSentryOptions.tunnelRoute}` : undefined; diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index fb228375f1e0..8382e66ca7d4 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -81,3 +81,40 @@ describe('applyTunnelRouteOption()', () => { expect(options.tunnel).toBe('/my-error-monitoring-route?o=2222222&p=3333333&r=us'); }); }); + +describe('Random tunnel route generation', () => { + it('Works when tunnelRoute is true and generates random-looking paths', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/abc123def'; // Simulated random path + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/abc123def?o=2222222&p=3333333'); + expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333$/); + }); + + it('Works with region DSNs when tunnelRoute is true', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/x7h9k2m'; // Simulated random path + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.eu.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/x7h9k2m?o=2222222&p=3333333&r=eu'); + expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333&r=eu$/); + }); + + it('Does not apply tunnel when tunnelRoute is false', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = undefined; + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBeUndefined(); + }); +}); From b94f65279c8341a7176fe68186feef58af57e2cb Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 18 Jun 2025 17:03:57 +0200 Subject: [PATCH 14/25] ci: Ensure CI uses correct node version & bump pnpm to latest v9 (#16645) Extracted this out of https://github.com/getsentry/sentry-javascript/pull/16644, for clarity --- .github/workflows/build.yml | 21 +++++-------------- .github/workflows/canary.yml | 10 ++------- .../test-applications/angular-20/package.json | 3 ++- package.json | 2 +- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94b1b007c912..980a1be55b61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -415,8 +415,6 @@ jobs: if: needs.job_build.outputs.changed_bun == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-24.04 - strategy: - fail-fast: false steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -442,8 +440,6 @@ jobs: if: needs.job_build.outputs.changed_deno == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-24.04 - strategy: - fail-fast: false steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -475,7 +471,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, '^24.0.1'] + node: [18, 20, 22, 24] steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v4 @@ -885,18 +881,11 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 9.4.0 - # TODO: Remove this once the repo is bumped to 20.19.2 or higher - - name: Set up Node for Angular 20 - if: matrix.test-application == 'angular-20' - uses: actions/setup-node@v4 - with: - node-version: '20.19.2' + version: 9.15.9 - name: Set up Node - if: matrix.test-application != 'angular-20' uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Set up Bun if: matrix.test-application == 'node-exports-test-app' uses: oven-sh/setup-bun@v2 @@ -1012,11 +1001,11 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 9.4.0 + version: 9.15.9 - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Restore caches uses: ./.github/actions/restore-cache with: diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 27abae270d73..49f603e8235c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -125,18 +125,12 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 9.4.0 - # TODO: Remove this once the repo is bumped to 20.19.2 or higher - - name: Set up Node for Angular 20 - if: matrix.test-application == 'angular-20' - uses: actions/setup-node@v4 - with: - node-version: '20.19.2' + version: 9.15.9 - name: Set up Node if: matrix.test-application != 'angular-20' uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Restore canary cache uses: actions/cache/restore@v4 diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json index 34ce69c6ea44..bfd14c64c93f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-20/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json @@ -46,6 +46,7 @@ "typescript": "~5.8.3" }, "volta": { - "extends": "../../package.json" + "extends": "../../package.json", + "node": "20.19.2" } } diff --git a/package.json b/package.json index 13e1a600e83d..db3e22a73769 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "volta": { "node": "20.18.2", "yarn": "1.22.22", - "pnpm": "9.15.0" + "pnpm": "9.15.9" }, "workspaces": [ "packages/angular", From 664461ab4ad79bb2594c8c150681e4cfb26bcacd Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Thu, 19 Jun 2025 23:18:24 +0200 Subject: [PATCH 15/25] feat: Add CLAUDE.md for Claude Code development guidance (#16660) ## Summary - Add comprehensive development guide for Claude Code - Include build commands, testing, and repository architecture - Document package management and monorepo structure ## Changes - Added `CLAUDE.md` with development workflows - Added Claude Code local settings configuration --------- Co-authored-by: Claude Co-authored-by: Abhijeet Prasad --- .claude/settings.local.json | 14 +++ .cursor/rules/sdk_development.mdc | 128 ++++++++++++++++++++++++++ CLAUDE.md | 147 ++++++++++++++++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .cursor/rules/sdk_development.mdc create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000000..a651eb363952 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(ls:*)", + "Bash(git:*)", + "Bash(yarn:*)", + "WebFetch(domain:github.com)", + "Bash(grep:*)", + "Bash(mv:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.cursor/rules/sdk_development.mdc b/.cursor/rules/sdk_development.mdc new file mode 100644 index 000000000000..988703ddac81 --- /dev/null +++ b/.cursor/rules/sdk_development.mdc @@ -0,0 +1,128 @@ +--- +description: Guidelines for working on the Sentry JavaScript SDK monorepo +alwaysApply: true +--- + +# SDK Development Rules + +You are working on the Sentry JavaScript SDK, a critical production SDK used by thousands of applications. Follow these rules strictly. + +## Code Quality Requirements (MANDATORY) + +**CRITICAL**: All changes must pass these checks before committing: + +1. **Always run `yarn lint`** - Fix all linting issues +2. **Always run `yarn test`** - Ensure all tests pass +3. **Always run `yarn build:dev`** - Verify TypeScript compilation + +## Development Commands + +### Build Commands +- `yarn build` - Full production build with package verification +- `yarn build:dev` - Development build (transpile + types) +- `yarn build:dev:watch` - Development build in watch mode (recommended) +- `yarn build:dev:filter ` - Build specific package and dependencies +- `yarn build:types:watch` - Watch mode for TypeScript types only +- `yarn build:bundle` - Build browser bundles only + +### Testing +- `yarn test` - Run all tests (excludes integration tests) +- `yarn test:unit` - Run unit tests only +- `yarn test:pr` - Run tests affected by changes (CI mode) +- `yarn test:pr:browser` - Run affected browser-specific tests +- `yarn test:pr:node` - Run affected Node.js-specific tests + +### Linting and Formatting +- `yarn lint` - Run ESLint and Prettier checks +- `yarn fix` - Auto-fix linting and formatting issues +- `yarn lint:es-compatibility` - Check ES compatibility + +## Git Flow Branching Strategy + +This repository uses **Git Flow**. See [docs/gitflow.md](docs/gitflow.md) for details. + +### Key Rules +- **All PRs target `develop` branch** (NOT `master`) +- `master` represents the last released state +- Never merge directly into `master` (except emergency fixes) +- Avoid changing `package.json` files on `develop` during pending releases + +### Branch Naming +- Features: `feat/descriptive-name` +- Releases: `release/X.Y.Z` + +## Repository Architecture + +This is a Lerna monorepo with 40+ packages in the `@sentry/*` namespace. + +### Core Packages +- `packages/core/` - Base SDK with interfaces, type definitions, core functionality +- `packages/types/` - Shared TypeScript type definitions (active) +- `packages/browser-utils/` - Browser-specific utilities and instrumentation + +### Platform SDKs +- `packages/browser/` - Browser SDK with bundled variants +- `packages/node/` - Node.js SDK with server-side integrations +- `packages/bun/`, `packages/deno/`, `packages/cloudflare/` - Runtime-specific SDKs + +### Framework Integrations +- Framework packages: `packages/{framework}/` (react, vue, angular, etc.) +- Client/server entry points where applicable (nextjs, nuxt, sveltekit) +- Integration tests use Playwright (Remix, browser-integration-tests) + +### User Experience Packages +- `packages/replay-internal/` - Session replay functionality +- `packages/replay-canvas/` - Canvas recording for replay +- `packages/replay-worker/` - Web worker support for replay +- `packages/feedback/` - User feedback integration + +### Development Packages (`dev-packages/`) +- `browser-integration-tests/` - Playwright browser tests +- `e2e-tests/` - End-to-end tests for 70+ framework combinations +- `node-integration-tests/` - Node.js integration tests +- `test-utils/` - Shared testing utilities +- `bundle-analyzer-scenarios/` - Bundle analysis +- `rollup-utils/` - Build utilities +- GitHub Actions packages for CI/CD automation + +## Development Guidelines + +### Build System +- Uses Rollup for bundling (`rollup.*.config.mjs`) +- TypeScript with multiple tsconfig files per package +- Lerna manages package dependencies and publishing +- Vite for testing with `vitest` + +### Package Structure Pattern +Each package typically contains: +- `src/index.ts` - Main entry point +- `src/sdk.ts` - SDK initialization logic +- `rollup.npm.config.mjs` - Build configuration +- `tsconfig.json`, `tsconfig.test.json`, `tsconfig.types.json` +- `test/` directory with corresponding test files + +### Key Development Notes +- Uses Volta for Node.js/Yarn version management +- Requires initial `yarn build` after `yarn install` for TypeScript linking +- Integration tests use Playwright extensively +- Native profiling requires Python <3.12 for binary builds + +## Testing Single Packages +- Test specific package: `cd packages/{package-name} && yarn test` +- Build specific package: `yarn build:dev:filter @sentry/{package-name}` + +## Code Style Rules +- Follow existing code conventions in each package +- Check imports and dependencies - only use libraries already in the codebase +- Look at neighboring files for patterns and style +- Never introduce code that exposes secrets or keys +- Follow security best practices + +## Before Every Commit Checklist +1. ✅ `yarn lint` (fix all issues) +2. ✅ `yarn test` (all tests pass) +3. ✅ `yarn build:dev` (builds successfully) +4. ✅ Target `develop` branch for PRs (not `master`) + +## Documentation Sync +**IMPORTANT**: When editing CLAUDE.md, also update .cursor/rules/sdk_development.mdc and vice versa to keep both files in sync. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000000..263b9eef97a4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,147 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Build Commands + +- `yarn build` - Full production build with package verification +- `yarn build:dev` - Development build (transpile + types) +- `yarn build:dev:watch` - Development build in watch mode (recommended for development) +- `yarn build:dev:filter ` - Build specific package and its dependencies +- `yarn build:types:watch` - Watch mode for TypeScript types only +- `yarn build:bundle` - Build browser bundles only + +### Testing + +- `yarn test` - Run all tests (excludes integration tests) +- `yarn test:unit` - Run unit tests only +- `yarn test:pr` - Run tests affected by changes (CI mode) +- `yarn test:pr:browser` - Run affected browser-specific tests +- `yarn test:pr:node` - Run affected Node.js-specific tests + +### Linting and Formatting + +- `yarn lint` - Run ESLint and Prettier checks +- `yarn fix` - Auto-fix linting and formatting issues +- `yarn lint:es-compatibility` - Check ES compatibility + +### Package Management + +- `yarn clean` - Clean build artifacts and caches +- `yarn clean:deps` - Clean and reinstall all dependencies + +## Repository Architecture + +This is a Lerna monorepo containing 40+ packages in the `@sentry/*` namespace. Key architectural components: + +### Core Packages + +- `packages/core/` - Base SDK with interfaces, type definitions, and core functionality +- `packages/types/` - Shared TypeScript type definitions (active) +- `packages/browser-utils/` - Browser-specific utilities and instrumentation + +### Platform SDKs + +- `packages/browser/` - Browser SDK with bundled variants +- `packages/node/` - Node.js SDK with server-side integrations +- `packages/bun/`, `packages/deno/`, `packages/cloudflare/` - Runtime-specific SDKs + +### Framework Integrations + +- Framework packages follow naming: `packages/{framework}/` (react, vue, angular, etc.) +- Each has client/server entry points where applicable (e.g., nextjs, nuxt, sveltekit) +- Integration tests use Playwright (e.g., Remix, browser-integration-tests) + +### User Experience Packages + +- `packages/replay-internal/` - Session replay functionality +- `packages/replay-canvas/` - Canvas recording support for replay +- `packages/replay-worker/` - Web worker support for replay +- `packages/feedback/` - User feedback integration + +### Build System + +- Uses Rollup for bundling with config files: `rollup.*.config.mjs` +- TypeScript with multiple tsconfig files per package (main, test, types) +- Lerna manages package dependencies and publishing +- Vite for testing with `vitest` + +### Package Structure Pattern + +Each package typically contains: + +- `src/index.ts` - Main entry point +- `src/sdk.ts` - SDK initialization logic +- `rollup.npm.config.mjs` - Build configuration +- `tsconfig.json`, `tsconfig.test.json`, `tsconfig.types.json` - TypeScript configs +- `test/` directory with corresponding test files + +### Development Packages (`dev-packages/`) + +Separate from main packages, containing development and testing utilities: + +- `browser-integration-tests/` - Playwright browser tests +- `e2e-tests/` - End-to-end tests for 70+ framework combinations +- `node-integration-tests/` - Node.js integration tests +- `test-utils/` - Shared testing utilities +- `bundle-analyzer-scenarios/` - Bundle analysis +- `rollup-utils/` - Build utilities +- GitHub Actions packages for CI/CD automation + +### Key Development Notes + +- Uses Volta for Node.js/Yarn version management +- Requires initial `yarn build` after `yarn install` for TypeScript linking +- Integration tests use Playwright extensively +- Native profiling requires Python <3.12 for binary builds +- Bundle outputs vary - check `build/bundles/` for specific files after builds + +## Git Flow Branching Strategy + +This repository uses **Git Flow** branching model. See [detailed documentation](docs/gitflow.md). + +### Key Points + +- **All PRs target `develop` branch** (not `master`) +- `master` represents the last released state +- Never merge directly into `master` (except emergency fixes) +- Automated workflow syncs `master` → `develop` after releases +- Avoid changing `package.json` files on `develop` during pending releases + +### Branch Naming + +- Features: `feat/descriptive-name` +- Releases: `release/X.Y.Z` + +## Code Quality Requirements + +**CRITICAL**: This is a production SDK used by thousands of applications. All changes must be: + +### Mandatory Checks + +- **Always run `yarn lint`** - Fix all linting issues before committing +- **Always run `yarn test`** - Ensure all tests pass +- **Run `yarn build`** - Verify build succeeds without errors + +### Before Any Commit + +1. `yarn lint` - Check and fix ESLint/Prettier issues +2. `yarn test` - Run relevant tests for your changes +3. `yarn build:dev` - Verify TypeScript compilation + +### CI/CD Integration + +- All PRs automatically run full lint/test/build pipeline +- Failed checks block merging +- Use `yarn test:pr` for testing only affected changes + +## Testing Single Packages + +To test a specific package: `cd packages/{package-name} && yarn test` +To build a specific package: `yarn build:dev:filter @sentry/{package-name}` + +## Cursor IDE Integration + +For Cursor IDE users, see [.cursor/rules/sdk_development.mdc](.cursor/rules/sdk_development.mdc) for complementary development rules. From 819d00e17e384e5adaa1a2e60dfb4cecd3eaa1d8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Jun 2025 08:05:56 -0400 Subject: [PATCH 16/25] fix(google-cloud-serverless): Make `CloudEvent` type compatible (#16661) resolves https://github.com/getsentry/sentry-javascript/issues/16653 As per the google cloud SDK (https://github.com/cloudevents/sdk-javascript/blob/c07afa9b774deefa137a960d2d11adee0bf3674a/src/event/interfaces.ts#L10-L31), `id` and `specversion` should be mandatory. --------- Co-authored-by: Cursor Agent --- .../google-cloud-serverless/src/gcpfunction/general.ts | 4 ++-- .../test/gcpfunction/cloud_event.test.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/google-cloud-serverless/src/gcpfunction/general.ts b/packages/google-cloud-serverless/src/gcpfunction/general.ts index f819bd5aaaf3..cae27ee6c2ee 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/general.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/general.ts @@ -29,10 +29,10 @@ export interface CloudFunctionsContext { export interface CloudEventsContext { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + id: string; + specversion: string; type?: string; - specversion?: string; source?: string; - id?: string; time?: string; schemaurl?: string; contenttype?: string; diff --git a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts index 758b37c0253b..4d34e630814c 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts @@ -45,6 +45,8 @@ describe('wrapCloudEventFunction', () => { function handleCloudEvent(fn: CloudEventFunctionWithCallback): Promise { return new Promise((resolve, reject) => { const context = { + id: 'test-event-id', + specversion: '1.0', type: 'event.type', }; @@ -232,6 +234,10 @@ describe('wrapCloudEventFunction', () => { const handler: CloudEventFunction = _context => 42; const wrappedHandler = wrapCloudEventFunction(handler); await handleCloudEvent(wrappedHandler); - expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' }); + expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { + id: 'test-event-id', + specversion: '1.0', + type: 'event.type', + }); }); }); From 439619656671711e23548330cf4a706703139d61 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Jun 2025 08:06:28 -0400 Subject: [PATCH 17/25] fix(browser): Remove usage of Array.at() method (#16647) Sentry SDK supported browsers: https://docs.sentry.io/platforms/javascript/troubleshooting/supported-browsers/ `.at` is ES2022: https://blog.saeloun.com/2022/02/24/ecmascript2022-adds-add-method/ The browser SDK supports ES2021 at the current moment, so we need to change this. Resolves https://github.com/getsentry/sentry-javascript/issues/16646 --- .../src/metrics/web-vitals/lib/LayoutShiftManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts index 76de0eb8290c..c9171b56ef0c 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts @@ -30,7 +30,8 @@ export class LayoutShiftManager { if (entry.hadRecentInput) return; const firstSessionEntry = this._sessionEntries[0]; - const lastSessionEntry = this._sessionEntries.at(-1); + // This previously used `this._sessionEntries.at(-1)` but that is ES2022. We support ES2021 and earlier. + const lastSessionEntry = this._sessionEntries[this._sessionEntries.length - 1]; // If the entry occurred less than 1 second after the previous entry // and less than 5 seconds after the first entry in the session, From c5613eb1ea24973d7b4bc199433ec6236427efa5 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Jun 2025 08:08:08 -0400 Subject: [PATCH 18/25] fix(core): Improve `safeJoin` usage in console logging integration (#16658) resolves https://github.com/getsentry/sentry-javascript/issues/16657 ## Background In the `consoleLoggingIntegration` (which sends logs to Sentry from console calls), we use a utility called `safeJoin` to join together the console args. https://github.com/getsentry/sentry-javascript/blob/b94f65279c8341a7176fe68186feef58af57e2cb/packages/core/src/utils/string.ts#L68-L94 This utility calls `String(value)` to convert items to a string, which results in objects and arrays being turned into the dreaded `[Object Object]` or `[Array]`. https://github.com/getsentry/sentry-javascript/blob/b94f65279c8341a7176fe68186feef58af57e2cb/packages/core/src/utils/string.ts#L86 A user wrote in with feedback that this was annoying, and I agree! ## Solution Instead of using `String(value)` I chose to create a new helper that uses `JSON.stringify(normalize(X))`, which should result in the entire object or array being properly shown. This required me to grab `normalizeDepth` and `normalizeMaxBreadth` from the client options, but that feels fine because it's easily explainable to users. I added tests to validate this. ## Next Steps We should really refactor our overall `safeJoin` usage. This should be safe in breadcrumbs, but will be a breaking change in `capture-console` as it'll cause issue grouping changes if console calls are being captured as messages/errors. --- .../public-api/logger/integration/subject.js | 5 +++ .../public-api/logger/integration/test.ts | 38 ++++++++++++++++++- packages/core/src/logs/console-integration.ts | 28 ++++++++++---- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js index 6c2e9cfdde7a..6974f191b76b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js @@ -6,6 +6,11 @@ console.warn('console.warn', 123, false); console.error('console.error', 123, false); console.assert(false, 'console.assert', 123, false); +// Test object and array truncation +console.log('Object:', { key: 'value', nested: { prop: 123 } }); +console.log('Array:', [1, 2, 3, 'string']); +console.log('Mixed:', 'prefix', { obj: true }, [4, 5, 6], 'suffix'); + console.log(''); Sentry.flush(); 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 e3aabc0d2fe6..7561b76e8b72 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 @@ -18,7 +18,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page expect(envelopeItems[0]).toEqual([ { type: 'log', - item_count: 8, + item_count: 11, content_type: 'application/vnd.sentry.items.log+json', }, { @@ -107,6 +107,42 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, }, }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Object: {"key":"value","nested":{"prop":123}}', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Array: [1,2,3,"string"]', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Mixed: prefix {"obj":true} [4,5,6] suffix', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, { timestamp: expect.any(Number), level: 'info', diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index 58460f59d9ab..677532c36346 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -5,8 +5,9 @@ import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { ConsoleLevel } from '../types-hoist/instrument'; import type { IntegrationFn } from '../types-hoist/integration'; +import { isPrimitive } from '../utils/is'; import { CONSOLE_LEVELS, logger } from '../utils/logger'; -import { safeJoin } from '../utils/string'; +import { normalize } from '../utils/normalize'; import { GLOBAL_OBJ } from '../utils/worldwide'; import { _INTERNAL_captureLog } from './exports'; @@ -32,7 +33,8 @@ const _consoleLoggingIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup(client) { - if (!client.getOptions()._experiments?.enableLogs) { + const { _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); + if (!_experiments?.enableLogs) { DEBUG_BUILD && logger.warn('`_experiments.enableLogs` is not enabled, ConsoleLogs integration disabled'); return; } @@ -45,9 +47,11 @@ const _consoleLoggingIntegration = ((options: Partial = { if (level === 'assert') { if (!args[0]) { const followingArgs = args.slice(1); - const message = - followingArgs.length > 0 ? `Assertion failed: ${formatConsoleArgs(followingArgs)}` : 'Assertion failed'; - _INTERNAL_captureLog({ level: 'error', message, attributes: DEFAULT_ATTRIBUTES }); + const assertionMessage = + followingArgs.length > 0 + ? `Assertion failed: ${formatConsoleArgs(followingArgs, normalizeDepth, normalizeMaxBreadth)}` + : 'Assertion failed'; + _INTERNAL_captureLog({ level: 'error', message: assertionMessage, attributes: DEFAULT_ATTRIBUTES }); } return; } @@ -55,7 +59,7 @@ const _consoleLoggingIntegration = ((options: Partial = { const isLevelLog = level === 'log'; _INTERNAL_captureLog({ level: isLevelLog ? 'info' : level, - message: formatConsoleArgs(args), + message: formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth), severityNumber: isLevelLog ? 10 : undefined, attributes: DEFAULT_ATTRIBUTES, }); @@ -85,8 +89,16 @@ const _consoleLoggingIntegration = ((options: Partial = { */ export const consoleLoggingIntegration = defineIntegration(_consoleLoggingIntegration); -function formatConsoleArgs(values: unknown[]): string { +function formatConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { return 'util' in GLOBAL_OBJ && typeof (GLOBAL_OBJ as GlobalObjectWithUtil).util.format === 'function' ? (GLOBAL_OBJ as GlobalObjectWithUtil).util.format(...values) - : safeJoin(values, ' '); + : safeJoinConsoleArgs(values, normalizeDepth, normalizeMaxBreadth); +} + +function safeJoinConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { + return values + .map(value => + isPrimitive(value) ? String(value) : JSON.stringify(normalize(value, normalizeDepth, normalizeMaxBreadth)), + ) + .join(' '); } From 04390581fb05c4f8e391fe3fb1039d840a3fce4d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Jun 2025 08:34:36 -0400 Subject: [PATCH 19/25] chore: Add cursor rule for publishing a release (#16662) We should give this a try next time we want to publish a release (ideally just kick off a background agent that does it). --------- Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- .cursor/rules/publishing_release.mdc | 32 +++++++++++++++++++++++++ docs/publishing-a-release.md | 35 ++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 .cursor/rules/publishing_release.mdc diff --git a/.cursor/rules/publishing_release.mdc b/.cursor/rules/publishing_release.mdc new file mode 100644 index 000000000000..bf801c7a7ff3 --- /dev/null +++ b/.cursor/rules/publishing_release.mdc @@ -0,0 +1,32 @@ +--- +description: Use this rule if you are looking to publish a release for the Sentry JavaScript SDKs +globs: +alwaysApply: false +--- +# Publishing a Release + +Use these guidelines when publishing a new Sentry JavaScript SDK release. + +## Standard Release Process (from develop to master) + +The release process is outlined in [publishing-a-release.md](mdc:docs/publishing-a-release.md). + +1. Make sure you are on the latest version of the `develop` branch. To confirm this, run `git pull origin develop` to get the latest changes from the repo. +2. Run `yarn changelog` on the `develop` branch and copy the output. You can use `yarn changelog | pbcopy` to copy the output of `yarn changelog` into your clipboard. +3. Decide on a version for the release based on [semver](mdc:https://semver.org). The version should be decided based on what is in included in the release. For example, if the release includes a new feature, we should increment the minor version. If it includes only bug fixes, we should increment the patch version. +4. Create a branch `prepare-release/VERSION`, eg. `prepare-release/8.1.0`, off `develop`. +5. Update [CHANGELOG.md](mdc:CHANGELOG.md) to add an entry for the next release number and a list of changes since the last release from the output of `yarn changelog`. See the `Updating the Changelog` section in [publishing-a-release.md](mdc:docs/publishing-a-release.md) for more details. If you remove changelog entries because they are not applicable, please let the user know. +6. Commit the changes to [CHANGELOG.md](mdc:CHANGELOG.md) with `meta(changelog): Update changelog for VERSION` where `VERSION` is the version of the release, e.g. `meta(changelog): Update changelog for 8.1.0` +7. Push the `prepare-release/VERSION` branch to origin and remind the user that the release PR needs to be opened from the `master` branch. + +## Key Commands + +- `yarn changelog` - Generate changelog entries +- `yarn lint` - Ensure code quality +- `yarn test` - Run test suite +- `yarn build:dev` - Verify build + +## Important Notes + +- This repository uses **Git Flow** - target `develop` for feature PRs, not `master`. See [gitflow.md](mdc:docs/gitflow.md) for more details. +- For first-time SDK releases, follow the New SDK Release Checklist [new-sdk-release-checklist.md](mdc:docs/new-sdk-release-checklist.md). If you notice there is something not following the new SDK release checklist, please remind the user. diff --git a/docs/publishing-a-release.md b/docs/publishing-a-release.md index 1ee26a12de77..3b952672e4e9 100644 --- a/docs/publishing-a-release.md +++ b/docs/publishing-a-release.md @@ -2,11 +2,13 @@ _These steps are only relevant to Sentry employees when preparing and publishing a new SDK release._ +These have also been documented via [Cursor Rules](../.cursor/rules/publishing-release.mdc). + **If you want to release a new SDK for the first time, be sure to follow the [New SDK Release Checklist](./new-sdk-release-checklist.md)** 1. Run `yarn changelog` on the `develop` branch and determine what version will be released (we use - [semver](https://semver.org)) + [semver](https://semver.org)). The semver version should be decided based on what is in included in the release. For example, if the release includes a new feature, we should increment the minor version. If it includes only bug fixes, we should increment the patch version. 2. Create a branch `prepare-release/VERSION`, eg. `prepare-release/8.1.0`, off develop 3. Update [`CHANGELOG.md`](https://github.com/getsentry/sentry-javascript/edit/master/CHANGELOG.md) to add an entry for the next release number and a list of changes since the last release. (See details below.) @@ -43,8 +45,9 @@ _These steps are only relevant to Sentry employees when preparing and publishing 2. Create a new section in the changelog with the previously determined version number. 3. Paste in the logs you copied earlier. 4. Delete any which aren't user-facing changes (such as docs or tests). -5. Highlight any important changes with subheadings. -6. If any of the PRs are from external contributors, include underneath the commits +5. If there are any important features or fixes, highlight them under the `Important Changes` subheading. If there are no important changes, don't include this section. If the `Important Changes` subheading is used, put all other changes under the `Other Changes` subheading. +6. Make sure the changelog entries are ordered alphabetically. +7. If any of the PRs are from external contributors, include underneath the commits `Work in this release contributed by . Thank you for your contributions!`. If there's only one external PR, don't forget to remove the final `s`. If there are three or more, use an Oxford comma. (It's in the Sentry styleguide!) @@ -52,4 +55,28 @@ _These steps are only relevant to Sentry employees when preparing and publishing "Unreleased". The GitHub Action creates a PR with this change every time a PR of an external contributor is merged. You can safely cut and paste this line to the new release section of the changelog (but a sanity check is never wrong). -7. Commit, push, and continue with step 4 from the previous section with the general instructions (above). +8. Commit, push, and continue with step 4 from the previous section with the general instructions (above). + +### Example Changelog Entry + +This is an example of a changelog entry for a release. + +```md +## 9.28.0 + +### Important Changes + +- **feat(nestjs): Stop creating spans for `TracingInterceptor` ([#16501](https://github.com/getsentry/sentry-javascript/pull/16501))** + +With this change we stop creating spans for `TracingInterceptor` as this interceptor only serves as an internal helper and adds noise for the user. + +- **feat(node): Update vercel ai spans as per new conventions ([#16497](https://github.com/getsentry/sentry-javascript/pull/16497))** + +This feature ships updates to the span names and ops to better match OpenTelemetry. This should make them more easily accessible to the new agents module view we are building. + +### Other Changes + +- fix(sveltekit): Export `vercelAIIntegration` from `@sentry/node` ([#16496](https://github.com/getsentry/sentry-javascript/pull/16496)) + +Work in this release was contributed by @agrattan0820. Thank you for your contribution! +``` From 4bbe6109ad8761ee934c1020b51cb1efa888caa5 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Jun 2025 08:51:02 -0400 Subject: [PATCH 20/25] feat(pino): Add initial package for `@sentry/pino-transport` (#16652) ref https://github.com/getsentry/sentry-javascript/issues/15952 resolves https://github.com/getsentry/sentry-javascript/issues/16621 We've gotten a lot of feedback about supporting pino. This PR kicks that off by creating a new pino package, specifically for the pino transport. All pino transports need to use https://github.com/pinojs/pino-abstract-transport, which we don't want to add as a dep to `@sentry/node`. Hopefully keeping the versions in sync shouldn't be too bad, although I'll force a peer dep range. --- .github/ISSUE_TEMPLATE/bug.yml | 1 + README.md | 2 + .../e2e-tests/verdaccio-config/config.yaml | 6 ++ package.json | 1 + packages/pino-transport/.eslintrc.js | 12 +++ packages/pino-transport/LICENSE | 16 ++++ packages/pino-transport/README.md | 31 ++++++++ packages/pino-transport/package.json | 77 +++++++++++++++++++ packages/pino-transport/rollup.npm.config.mjs | 3 + packages/pino-transport/src/index.ts | 2 + packages/pino-transport/test/index.test.ts | 8 ++ packages/pino-transport/tsconfig.json | 10 +++ packages/pino-transport/tsconfig.test.json | 7 ++ packages/pino-transport/tsconfig.types.json | 10 +++ packages/pino-transport/vite.config.ts | 7 ++ 15 files changed, 193 insertions(+) create mode 100644 packages/pino-transport/.eslintrc.js create mode 100644 packages/pino-transport/LICENSE create mode 100644 packages/pino-transport/README.md create mode 100644 packages/pino-transport/package.json create mode 100644 packages/pino-transport/rollup.npm.config.mjs create mode 100644 packages/pino-transport/src/index.ts create mode 100644 packages/pino-transport/test/index.test.ts create mode 100644 packages/pino-transport/tsconfig.json create mode 100644 packages/pino-transport/tsconfig.test.json create mode 100644 packages/pino-transport/tsconfig.types.json create mode 100644 packages/pino-transport/vite.config.ts diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4a2d16e13598..ba0bdd8df4c3 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -50,6 +50,7 @@ body: - '@sentry/nestjs' - '@sentry/nextjs' - '@sentry/nuxt' + - '@sentry/pino-transport' - '@sentry/react' - '@sentry/react-router' - '@sentry/remix' diff --git a/README.md b/README.md index 8b22dafb0c63..f92fd89ec716 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ below: Provides the integration for Session Replay. - [`@sentry/core`](https://github.com/getsentry/sentry-javascript/tree/master/packages/core): The base for all JavaScript SDKs with interfaces, type definitions and base classes. +- [`@sentry/pino-transport`](https://github.com/getsentry/sentry-javascript/tree/master/packages/pino-transport): Pino + transport for automatically sending log messages to Sentry. ## Bug Bounty Program diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 8535c5898175..c5400118d12c 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -110,6 +110,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/pino-transport': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/profiling-node': access: $all publish: $all diff --git a/package.json b/package.json index db3e22a73769..75bd3b8f8380 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "packages/node", "packages/nuxt", "packages/opentelemetry", + "packages/pino-transport", "packages/profiling-node", "packages/react", "packages/react-router", diff --git a/packages/pino-transport/.eslintrc.js b/packages/pino-transport/.eslintrc.js new file mode 100644 index 000000000000..01c6be4c7080 --- /dev/null +++ b/packages/pino-transport/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['src/**/*.ts'], + rules: {}, + }, + ], +}; diff --git a/packages/pino-transport/LICENSE b/packages/pino-transport/LICENSE new file mode 100644 index 000000000000..5251db3eaaca --- /dev/null +++ b/packages/pino-transport/LICENSE @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2025 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/pino-transport/README.md b/packages/pino-transport/README.md new file mode 100644 index 000000000000..45480f802cb1 --- /dev/null +++ b/packages/pino-transport/README.md @@ -0,0 +1,31 @@ +# @sentry/pino-transport + +[![npm version](https://img.shields.io/npm/v/@sentry/pino-transport.svg)](https://www.npmjs.com/package/@sentry/pino-transport) +[![npm dm](https://img.shields.io/npm/dm/@sentry/pino-transport.svg)](https://www.npmjs.com/package/@sentry/pino-transport) +[![npm dt](https://img.shields.io/npm/dt/@sentry/pino-transport.svg)](https://www.npmjs.com/package/@sentry/pino-transport) + +**This package is currently in alpha. Breaking changes may still occur.** + +A Pino transport for integrating [Pino](https://github.com/pinojs/pino) logging with [Sentry](https://sentry.io). This transport automatically captures log messages as Sentry events and breadcrumbs, making it easy to monitor your application's logs in Sentry. + +## Installation + +```bash +npm install @sentry/node @sentry/pino-transport +# or +yarn add @sentry/node @sentry/pino-transport +``` + +## Usage + +TODO: Add usage instructions + +## Requirements + +- Node.js 18 or higher +- Pino 8.0.0 or higher +- @sentry/node must be configured in your application + +## License + +MIT diff --git a/packages/pino-transport/package.json b/packages/pino-transport/package.json new file mode 100644 index 000000000000..0fdf499c306c --- /dev/null +++ b/packages/pino-transport/package.json @@ -0,0 +1,77 @@ +{ + "name": "@sentry/pino-transport", + "version": "9.30.0", + "description": "Pino transport for Sentry SDK", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/pino-transport", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "files": [ + "/build" + ], + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "typesVersions": { + "<5.0": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "9.30.0" + }, + "peerDependencies": { + "pino": "^8.0.0 || ^9.0.0" + }, + "devDependencies": { + "@types/node": "^18.19.1", + "pino": "^9.0.0" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "npm pack", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage sentry-pino-transport-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", + "test": "yarn test:unit", + "test:unit": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/pino-transport/rollup.npm.config.mjs b/packages/pino-transport/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/pino-transport/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/pino-transport/src/index.ts b/packages/pino-transport/src/index.ts new file mode 100644 index 000000000000..849aa99b4f7c --- /dev/null +++ b/packages/pino-transport/src/index.ts @@ -0,0 +1,2 @@ +// TODO: Implement this +export {}; diff --git a/packages/pino-transport/test/index.test.ts b/packages/pino-transport/test/index.test.ts new file mode 100644 index 000000000000..9329d9cbaede --- /dev/null +++ b/packages/pino-transport/test/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest'; +import * as index from '../src'; + +describe('pino-transport', () => { + it('should be defined', () => { + expect(index).toBeDefined(); + }); +}); diff --git a/packages/pino-transport/tsconfig.json b/packages/pino-transport/tsconfig.json new file mode 100644 index 000000000000..b9683a850600 --- /dev/null +++ b/packages/pino-transport/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + "lib": ["es2018", "es2020.string"], + "module": "Node16" + } +} diff --git a/packages/pino-transport/tsconfig.test.json b/packages/pino-transport/tsconfig.test.json new file mode 100644 index 000000000000..4c24dbbea96e --- /dev/null +++ b/packages/pino-transport/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["test/**/*", "src/**/*", "vite.config.ts"], + "compilerOptions": { + "types": ["vitest/globals", "node"] + } +} diff --git a/packages/pino-transport/tsconfig.types.json b/packages/pino-transport/tsconfig.types.json new file mode 100644 index 000000000000..f35cdd6b5d81 --- /dev/null +++ b/packages/pino-transport/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "build/types", + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "stripInternal": true + } +} diff --git a/packages/pino-transport/vite.config.ts b/packages/pino-transport/vite.config.ts new file mode 100644 index 000000000000..4ac6027d5789 --- /dev/null +++ b/packages/pino-transport/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +}); From 797ebd1426512092d6c4fbd328b9e9ee7da28101 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Fri, 20 Jun 2025 11:41:29 -0700 Subject: [PATCH 21/25] fix: Wait for the correct clientWidth/clientHeight when showing Feedback Screenshot previews (#16648) Before we would see a blank screenshot in the feedback modal when using firefox. After resizing the screenshot would appear. I debugged and foundout that the clientWidth/clientHeight wasn't being set right away, therefore the elements were on the page properly, and contained the correct pixel information drawn into the canvas, but the canvas wasn't sized correctly on the page, so you couldn't see anything. This triggers a resize after a tick, giving the browser a chance first set the width/height before we measure it and set the scale factor. **Before:** SCR-20250618-kgwr After: SCR-20250618-kgrm Fixes REPLAY-420 --- .../feedback/src/screenshot/components/ScreenshotEditor.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx index ae3d0f653dd8..083f5c16dec6 100644 --- a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx +++ b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx @@ -166,6 +166,11 @@ export function ScreenshotEditorFactory({ ); setScaleFactor(scale); }); + + // For Firefox, the canvas is not yet measured, so we need to wait for it to get the correct size + if (measurementDiv.clientHeight === 0 || measurementDiv.clientWidth === 0) { + setTimeout(handleResize, 0); + } }; handleResize(); From 71e403775b7b0c73bccf1737c5354ce44300dcfa Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 23 Jun 2025 09:40:43 +0200 Subject: [PATCH 22/25] deps: Update all bundler plugin instances to latest & allow caret ranges (#16641) If a version is released with a buggy bundler plugin dependency pinned, it can lead to problems. IMHO it should be fine to allow `^` ranges there to ensure bugfixes etc. are pulled in? Or is there a good reason not to do that here? API should be stable in this regard. Note: Astro& solidstart remain on `@sentry/vite-plugin` v2.x as that has more stuff exposed, did not want to get into that right now... something for v10? --- packages/gatsby/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nuxt/package.json | 4 ++-- packages/react-router/package.json | 2 +- packages/sveltekit/package.json | 2 +- yarn.lock | 34 ++++++------------------------ 6 files changed, 13 insertions(+), 33 deletions(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 919b05974986..2b5606a75db7 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "9.30.0", "@sentry/react": "9.30.0", - "@sentry/webpack-plugin": "3.5.0" + "@sentry/webpack-plugin": "^3.5.0" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 2c881cce3993..ff770802eed9 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -85,7 +85,7 @@ "@sentry/opentelemetry": "9.30.0", "@sentry/react": "9.30.0", "@sentry/vercel-edge": "9.30.0", - "@sentry/webpack-plugin": "3.5.0", + "@sentry/webpack-plugin": "^3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "4.35.0", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index c97a63d39844..66f8affd4caf 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -47,8 +47,8 @@ "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", "@sentry/opentelemetry": "9.30.0", - "@sentry/rollup-plugin": "3.5.0", - "@sentry/vite-plugin": "3.2.4", + "@sentry/rollup-plugin": "^3.5.0", + "@sentry/vite-plugin": "^3.5.0", "@sentry/vue": "9.30.0" }, "devDependencies": { diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 9a3e4954c07c..98f4906546ec 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -43,7 +43,7 @@ "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", "@sentry/react": "9.30.0", - "@sentry/vite-plugin": "^3.2.4", + "@sentry/vite-plugin": "^3.5.0", "glob": "11.0.1" }, "devDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index ba7a4afd4a0c..b2f1527c708b 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -53,7 +53,7 @@ "@sentry/node": "9.30.0", "@sentry/opentelemetry": "9.30.0", "@sentry/svelte": "9.30.0", - "@sentry/vite-plugin": "3.2.4", + "@sentry/vite-plugin": "^3.5.0", "magic-string": "0.30.7", "recast": "0.23.11", "sorcery": "1.0.0" diff --git a/yarn.lock b/yarn.lock index ce3e50da8c41..933b58556d7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6566,11 +6566,6 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== -"@sentry/babel-plugin-component-annotate@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz#c0877df6e5ce227bf51754bf27da2fa5227af847" - integrity sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA== - "@sentry/babel-plugin-component-annotate@3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz#1b0d01f903b725da876117d551610085c3dd21c7" @@ -6590,20 +6585,6 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.4.tgz#4d892490be3cbb127c7c4ed00fcd1b525129fb1e" - integrity sha512-YMj9XW5W2JA89EeweE7CPKLDz245LBsI1JhCmqpt/bjSvmsSIAAPsLYnvIJBS3LQFm0OhtG8NB54PTi96dAcMA== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.2.4" - "@sentry/cli" "2.42.2" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - "@sentry/bundler-plugin-core@3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz#b62af5be1b1a862e7062181655829c556c7d7c0b" @@ -6732,7 +6713,7 @@ "@sentry/cli-win32-i686" "2.46.0" "@sentry/cli-win32-x64" "2.46.0" -"@sentry/rollup-plugin@3.5.0": +"@sentry/rollup-plugin@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-3.5.0.tgz#9015c48e00257f8440597167498499804371329b" integrity sha512-aMPCvdNMkv//LZYjYCJsEcNiNiaQFinBO75+9NJVEe1OrKNdGqDi3hky2ll7zuY+xozEtZCZcUKJJz/aAYAS8A== @@ -6748,15 +6729,15 @@ "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" -"@sentry/vite-plugin@3.2.4", "@sentry/vite-plugin@^3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-3.2.4.tgz#da87534d645c116ef579161d44636a34eee41a25" - integrity sha512-ZRn5TLlq5xtwKOqaWP+XqS1PYVfbBCgsbMk7wW2Ly6EgF9wYePvtLqKgYnE3hwPg2LpBnRPR2ti1ohlUkR+wXA== +"@sentry/vite-plugin@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz#138fc535c97e69eb8032d57c02aba9c161c7654a" + integrity sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg== dependencies: - "@sentry/bundler-plugin-core" "3.2.4" + "@sentry/bundler-plugin-core" "3.5.0" unplugin "1.0.1" -"@sentry/webpack-plugin@3.5.0": +"@sentry/webpack-plugin@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz#cde95534f1e945a4002d47465aeda01d382cd279" integrity sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ== @@ -27148,7 +27129,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From f0cad82fea5ec6fd8f818ec4a7c40a1693360e34 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 23 Jun 2025 10:39:07 +0200 Subject: [PATCH 23/25] deps: Remove unused `@sentry/opentelemetry` dependency (#16677) Remove the unused dependency in: * nuxt * solidstart * remix * bun * sveltekit * tanstackstart-react Closes https://github.com/getsentry/sentry-javascript/issues/16676 --- packages/bun/package.json | 3 +- packages/nuxt/package.json | 1 - packages/remix/package.json | 1 - packages/remix/test/integration/package.json | 1 - packages/solidstart/package.json | 1 - packages/sveltekit/package.json | 1 - packages/tanstackstart-react/package.json | 1 - yarn.lock | 75 +++++++++++++++++++- 8 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/bun/package.json b/packages/bun/package.json index 1beafadb1baf..80048fb579f9 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -40,8 +40,7 @@ }, "dependencies": { "@sentry/core": "9.30.0", - "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0" + "@sentry/node": "9.30.0" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 66f8affd4caf..d9dbd9a531b6 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -46,7 +46,6 @@ "@sentry/browser": "9.30.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/rollup-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0", "@sentry/vue": "9.30.0" diff --git a/packages/remix/package.json b/packages/remix/package.json index c63cdd69e82f..102eb9f226d6 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -71,7 +71,6 @@ "@sentry/cli": "^2.46.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/react": "9.30.0", "glob": "^10.3.4", "yargs": "^17.6.0" diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 7f4172b0b42f..4e15e7de7398 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -27,7 +27,6 @@ "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", "@sentry/node": "file:../../../node", - "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index c1d8f7d88022..1d6033ee6f9a 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -68,7 +68,6 @@ "dependencies": { "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/solid": "9.30.0", "@sentry/vite-plugin": "2.22.6" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index b2f1527c708b..c80789eb32f4 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -51,7 +51,6 @@ "@sentry/cloudflare": "9.30.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/svelte": "9.30.0", "@sentry/vite-plugin": "^3.5.0", "magic-string": "0.30.7", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index ccfbefa37bbb..2abd5e0fbedc 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -55,7 +55,6 @@ "@sentry-internal/browser-utils": "9.30.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/react": "9.30.0" }, "scripts": { diff --git a/yarn.lock b/yarn.lock index 933b58556d7f..5c958751e59d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10245,6 +10245,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.20, autoprefixer@^10.4.8: version "10.4.20" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" @@ -15662,6 +15667,11 @@ fast-printf@^1.6.9: dependencies: boolean "^3.1.4" +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + fast-safe-stringify@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" @@ -22386,6 +22396,11 @@ ohash@^1.1.3, ohash@^1.1.4: resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -23300,6 +23315,35 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^9.0.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.7.0.tgz#ff7cd86eb3103ee620204dbd5ca6ffda8b53f645" + integrity sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pirates@^4.0.1: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -24219,6 +24263,11 @@ process-relative-require@^1.0.0: dependencies: node-modules-path "^1.0.0" +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -24401,6 +24450,11 @@ queue-tick@^1.0.1: resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -24791,6 +24845,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + realistic-structured-clone@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-3.0.0.tgz#7b518049ce2dad41ac32b421cd297075b00e3e35" @@ -26408,6 +26467,13 @@ solid-use@^0.8.0: resolved "https://registry.yarnpkg.com/solid-use/-/solid-use-0.8.0.tgz#d46258c45edb0f4c621285e0ad1aa6b6a674d79b" integrity sha512-YX+XmcKLvSx3bwMimMhFy40ZkDnShnUcEw6cW6fSscwKEgl1TG3GlgAvkBmQ3AeWjvQSd8+HGTr82ImsrjkkqA== +sonic-boom@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" + integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== + dependencies: + atomic-sleep "^1.0.0" + sorcery@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-1.0.0.tgz#b5bb81fb9706c0c240f5f2d3214b4d2be649e07f" @@ -26656,7 +26722,7 @@ split2@^3.2.2: dependencies: readable-stream "^3.0.0" -split2@^4.1.0: +split2@^4.0.0, split2@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== @@ -27546,6 +27612,13 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + throttleit@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4" From d8f12c23055cd1a7d6fed2d6ac7a925665a2ca89 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 23 Jun 2025 13:04:24 +0200 Subject: [PATCH 24/25] fix(node): Ensure graphql errors result in errored spans (#16678) Closes https://github.com/getsentry/sentry-javascript/issues/16649 --- .../tracing/apollo-graphql/scenario-error.js | 43 +++++++++++++++++++ .../suites/tracing/apollo-graphql/test.ts | 25 +++++++++++ .../node/src/integrations/tracing/graphql.ts | 10 ++++- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js new file mode 100644 index 000000000000..99d091325720 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js @@ -0,0 +1,43 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +async function run() { + const { gql } = require('apollo-server'); + const server = require('./apollo-server')(); + + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: gql` + mutation Mutation($email: String) { + login(email: $email) + } + `, + // We want to trigger an error by passing an invalid variable type + variables: { email: 123 }, + }); + + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts index 2abe2932ece2..4d6661101dc0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts @@ -55,4 +55,29 @@ describe('GraphQL/Apollo Tests', () => { .start() .completed(); }); + + test('should handle GraphQL errors.', async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction (mutation Mutation)', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'graphql.operation.name': 'Mutation', + 'graphql.operation.type': 'mutation', + 'graphql.source': 'mutation Mutation($email: String) {\n login(email: $email)\n}', + 'sentry.origin': 'auto.graphql.otel.graphql', + }, + description: 'mutation Mutation', + status: 'unknown_error', + origin: 'auto.graphql.otel.graphql', + }), + ]), + }; + + await createRunner(__dirname, 'scenario-error.js') + .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); }); diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts index 841baf46754d..5301ad5180b0 100644 --- a/packages/node/src/integrations/tracing/graphql.ts +++ b/packages/node/src/integrations/tracing/graphql.ts @@ -1,4 +1,5 @@ import type { AttributeValue } from '@opentelemetry/api'; +import { SpanStatusCode } from '@opentelemetry/api'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, getRootSpan, spanToJSON } from '@sentry/core'; @@ -45,9 +46,16 @@ export const instrumentGraphql = generateInstrumentOnce( return { ...options, - responseHook(span) { + responseHook(span, result) { addOriginToSpan(span, 'auto.graphql.otel.graphql'); + // We want to ensure spans are marked as errored if there are errors in the result + // We only do that if the span is not already marked with a status + const resultWithMaybeError = result as { errors?: { message: string }[] }; + if (resultWithMaybeError.errors?.length && !spanToJSON(span).status) { + span.setStatus({ code: SpanStatusCode.ERROR }); + } + const attributes = spanToJSON(span).data; // If operation.name is not set, we fall back to use operation.type only From 58337eae02739d94e620316d6381b4a49fdcc7a5 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 23 Jun 2025 13:23:13 +0200 Subject: [PATCH 25/25] meta(changelog): Update changelog for 9.31.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index baf79461528f..349cd0b3ce71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.31.0 + +### Important Changes + +- feat(nextjs): Add option for auto-generated random tunnel route ([#16626](https://github.com/getsentry/sentry-javascript/pull/16626)) + +Adds an option to automatically generate a random tunnel route for the Next.js SDK. This helps prevent ad blockers and other tools from blocking Sentry requests by using a randomized path instead of the predictable `/monitoring` endpoint. + +- feat(core): Allow to pass `scope` & `client` to `getTraceData` ([#16633](https://github.com/getsentry/sentry-javascript/pull/16633)) + +Adds the ability to pass custom `scope` and `client` parameters to the `getTraceData` function, providing more flexibility when generating trace data for distributed tracing. + +### Other Changes + +- deps: Remove unused `@sentry/opentelemetry` dependency ([#16677](https://github.com/getsentry/sentry-javascript/pull/16677)) +- deps: Update all bundler plugin instances to latest & allow caret ranges ([#16641](https://github.com/getsentry/sentry-javascript/pull/16641)) +- feat(deps): Bump @prisma/instrumentation from 6.8.2 to 6.9.0 ([#16608](https://github.com/getsentry/sentry-javascript/pull/16608)) +- feat(flags): add node support for generic featureFlagsIntegration and move utils to core ([#16585](https://github.com/getsentry/sentry-javascript/pull/16585)) +- feat(flags): capture feature flag evaluations on spans ([#16485](https://github.com/getsentry/sentry-javascript/pull/16485)) +- feat(pino): Add initial package for `@sentry/pino-transport` ([#16652](https://github.com/getsentry/sentry-javascript/pull/16652)) +- fix: Wait for the correct clientWidth/clientHeight when showing Feedback Screenshot previews ([#16648](https://github.com/getsentry/sentry-javascript/pull/16648)) +- fix(browser): Remove usage of Array.at() method ([#16647](https://github.com/getsentry/sentry-javascript/pull/16647)) +- fix(core): Improve `safeJoin` usage in console logging integration ([#16658](https://github.com/getsentry/sentry-javascript/pull/16658)) +- fix(google-cloud-serverless): Make `CloudEvent` type compatible ([#16661](https://github.com/getsentry/sentry-javascript/pull/16661)) +- fix(nextjs): Fix lookup of `instrumentation-client.js` file ([#16637](https://github.com/getsentry/sentry-javascript/pull/16637)) +- fix(node): Ensure graphql errors result in errored spans ([#16678](https://github.com/getsentry/sentry-javascript/pull/16678)) + ## 9.30.0 - feat(nextjs): Add URL to tags of server components and generation functions issues ([#16500](https://github.com/getsentry/sentry-javascript/pull/16500))