diff --git a/.changeset/tricky-seahorses-float.md b/.changeset/tricky-seahorses-float.md new file mode 100644 index 00000000..1f32afed --- /dev/null +++ b/.changeset/tricky-seahorses-float.md @@ -0,0 +1,17 @@ +--- +'@spotlightjs/overlay': minor +'@spotlightjs/sidecar': minor +--- + +Add base64 encoding for envelope passing + +This fixes the issue certain characters getting lost or changed during the implicit +and forced UTF-8 encoding, namely certain ANSI-escape characters when we capture +them as breadcrumbs. This was breaking NextJS recently. + +The mechanism is opt-in from Sidecar side and the new overlay automatically opts +in to fix the issue. The new overlay is also capable of processing messages w/o +base64 encoding so this change is both backwards and forwards compatible meaning +a new version of overlay can work with an old sidecar and a new version of sidecar +can work with an older overlay. That said to get the fix, both should be on the new +version, opting into base64 encoding. diff --git a/packages/overlay/_fixtures/envelope_with_nextjs_ansi_escapes.txt b/packages/overlay/_fixtures/envelope_with_nextjs_ansi_escapes.txt new file mode 100644 index 00000000..e3c4cd77 --- /dev/null +++ b/packages/overlay/_fixtures/envelope_with_nextjs_ansi_escapes.txt @@ -0,0 +1,3 @@ +{"event_id":"7d644a7be9c14568b3affcfb7f527c34","sent_at":"2025-01-17T15:49:22.219Z","sdk":{"name":"sentry.javascript.nextjs","version":"8.50.0"},"trace":{"environment":"development","trace_id":"66db533aac7bfbb6023647914ccfadc5","sample_rate":"1","transaction":"POST /api/error","sampled":"true"}} +{"type":"event"} +{"exception":{"values":[{"type":"Error","value":"Error on API route (POST)","stacktrace":{"frames":[{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.js","module":"next.dist.server:base-server","function":"DevServer.renderToResponseWithComponents","lineno":1067,"colno":41,"in_app":false,"pre_context":[" // `staticPaths` is intentionally set to `undefined` as it should've"," // been caught when checking disk data."," staticPaths: undefined,"," fallbackMode: (0, _fallback.parseFallbackField)(fallbackField)"," };"," }"," async renderToResponseWithComponents(requestContext, findComponentsResult) {"],"context_line":" return (0, _tracer.getTracer)().trace(_constants1.BaseServerSpan.renderToResponseWithComponents, async ()=>this.renderToResponseWith {snip}","post_context":[" }"," pathCouldBeIntercepted(resolvedPathname) {"," return (0, _interceptionroutes.isInterceptionRouteAppPath)(resolvedPathname) || this.interceptionRoutePatterns.some((regexp)=>{"," return regexp.test(resolvedPathname);"," });"," }"," setVaryHeader(req, res, isAppPath, resolvedPathname) {"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/trace/tracer.js","module":"next.dist.server.lib.trace:tracer","function":"NextTracerImpl.trace","lineno":134,"colno":20,"in_app":false,"pre_context":[" fn: fnOrEmpty,"," options: {"," ...fnOrOptions"," }"," };"," const spanName = options.spanName ?? type;"," if (!_constants.NextVanillaSpanAllowlist.includes(type) && process.env.NEXT_OTEL_VERBOSE !== '1' || options.hideSpan) {"],"context_line":" return fn();","post_context":[" }"," // Trying to get active scoped span to assign parent. If option specifies parent span manually, will try to use it."," let spanContext = this.getSpanContext((options == null ? void 0 : options.parentSpan) ?? this.getActiveScopeSpan());"," let isRootSpan = false;"," if (!spanContext) {"," spanContext = (context == null ? void 0 : context.active()) ?? ROOT_CONTEXT;"," isRootSpan = true;"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.js","module":"next.dist.server:base-server","function":"?","lineno":1067,"colno":121,"in_app":false,"pre_context":[" // `staticPaths` is intentionally set to `undefined` as it should've"," // been caught when checking disk data."," staticPaths: undefined,"," fallbackMode: (0, _fallback.parseFallbackField)(fallbackField)"," };"," }"," async renderToResponseWithComponents(requestContext, findComponentsResult) {"],"context_line":" return (0, _tracer.getTracer)().trace(_constants1.BaseServerSpan.renderToResponseWithComponents, async ()=>this.renderToResponseWith {snip}","post_context":[" }"," pathCouldBeIntercepted(resolvedPathname) {"," return (0, _interceptionroutes.isInterceptionRouteAppPath)(resolvedPathname) || this.interceptionRoutePatterns.some((regexp)=>{"," return regexp.test(resolvedPathname);"," });"," }"," setVaryHeader(req, res, isAppPath, resolvedPathname) {"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.js","module":"next.dist.server:base-server","function":"DevServer.renderToResponseWithComponentsImpl","lineno":1832,"colno":53,"in_app":false,"pre_context":[" });"," if (!result) return null;"," return {"," ...result,"," revalidate: result.revalidate"," };"," };"],"context_line":" const cacheEntry = await this.responseCache.get(ssgCacheKey, responseGenerator, {","post_context":[" routeKind: // If the route module is not defined, we can assume it's a page being"," // rendered and thus check isAppPath."," (routeModule == null ? void 0 : routeModule.definition.kind) ?? (isAppPath ? _routekind.RouteKind.APP_PAGE : _routekind.RouteKind.PAGES),"," incrementalCache,"," isOnDemandRevalidate,"," isPrefetch: req.headers.purpose === 'prefetch',"," isRoutePPREnabled"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/response-cache/index.js","module":"next.dist.server.response-cache:index","function":"ResponseCache.get","lineno":49,"colno":20,"in_app":false,"pre_context":[" const minimalModeKey = 'minimalMode';"," this[minimalModeKey] = minimalMode;"," }"," async get(key, responseGenerator, context) {"," // If there is no key for the cache, we can't possibly look this up in the"," // cache so just return the result of the response generator."," if (!key) {"],"context_line":" return responseGenerator({","post_context":[" hasResolved: false,"," previousCacheEntry: null"," });"," }"," const { incrementalCache, isOnDemandRevalidate = false, isFallback = false, isRoutePPREnabled = false } = context;"," const response = await this.batcher.batch({"," key,"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.js","module":"next.dist.server:base-server","function":"responseGenerator","lineno":1822,"colno":34,"in_app":false,"pre_context":[" };"," }"," // If this is a dynamic route with PPR enabled and the default route"," // matches were set, then we should pass the fallback route params to"," // the renderer as this is a fallback revalidation request."," const fallbackRouteParams = isDynamic && isRoutePPREnabled && ((0, _requestmeta.getRequestMeta)(req, 'didSetDefaultRouteMatches' {snip}"," // Perform the render."],"context_line":" const result = await doRender({","post_context":[" postponed,"," fallbackRouteParams"," });"," if (!result) return null;"," return {"," ...result,"," revalidate: result.revalidate"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.js","module":"next.dist.server:base-server","function":"doRender","lineno":1452,"colno":60,"in_app":false,"pre_context":[" onAfterTaskError: undefined,"," onInstrumentationRequestError: this.renderOpts.onInstrumentationRequestError,"," buildId: this.renderOpts.buildId"," }"," };"," try {"," const request = _nextrequest.NextRequestAdapter.fromNodeNextRequest(req, (0, _nextrequest.signalFromNodeResponse)(re {snip}"],"context_line":" const response = await routeModule.handle(request, context);","post_context":[" req.fetchMetrics = context.renderOpts.fetchMetrics;"," const cacheTags = context.renderOpts.collectedTags;"," // If the request is for a static response, we can cache it so long"," // as it's not edge."," if (isSSG) {"," const blob = await response.blob();"," // Copy the headers from the response."]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js","module":"next.dist.compiled.next-server:app-route.runtime.dev","function":"AppRouteRouteModule.handle","lineno":10,"colno":39855,"in_app":false},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js","module":"next.dist.compiled.next-server:app-route.runtime.dev","function":"?","lineno":10,"colno":39901,"in_app":false},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js","module":"next.dist.compiled.next-server:app-route.runtime.dev","function":"?","lineno":10,"colno":39944,"in_app":false},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js","module":"next.dist.compiled.next-server:app-route.runtime.dev","function":"?","lineno":10,"colno":42132,"in_app":false},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/trace/tracer.js","module":"next.dist.server.lib.trace:tracer","function":"NextTracerImpl.trace","lineno":151,"colno":28,"in_app":false,"pre_context":[" }"," const spanId = getSpanId();"," options.attributes = {"," 'next.span_name': spanName,"," 'next.span_type': type,"," ...options.attributes"," };"],"context_line":" return context.with(spanContext.setValue(rootSpanIdKey, spanId), ()=>this.getTracerInstance().startActiveSpan(spanName, options, (span)=>{","post_context":[" const startTime = 'performance' in globalThis && 'measure' in performance ? globalThis.performance.now() : undefined;"," const onCleanup = ()=>{"," rootSpanAttributesStore.delete(spanId);"," if (startTime && process.env.NEXT_OTEL_PERFORMANCE_PREFIX && _constants.LogSpanAllowList.includes(type || '')) {"," performance.measure(`${process.env.NEXT_OTEL_PERFORMANCE_PREFIX}:next-${(type.split('.').pop() || '').replace(/[A-Z] {snip}"," start: startTime,"," end: performance.now()"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/src/api/context.js","module":"@opentelemetry.api.build.src.api:context","function":"ContextAPI.with","lineno":60,"colno":46,"in_app":false,"pre_context":[" *"," * @param context context to be active during function execution"," * @param fn function to execute in a context"," * @param thisArg optional receiver to be used for calling fn"," * @param args optional arguments forwarded to fn"," */"," with(context, fn, thisArg, ...args) {"],"context_line":" return this._getContextManager().with(context, fn, thisArg, ...args);","post_context":[" }"," /**"," * Bind a context to a target function or event emitter"," *"," * @param context context to bind to the event emitter or function. Defaults to the currently active context"," * @param target function or event emitter to bind"," */"]},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"SentryContextManager.with","lineno":1478,"colno":1,"in_app":false,"pre_context":[" setContextOnScope(newCurrentScope, ctx2);",""],"context_line":" return super.with(ctx2, fn, thisArg, ...args);","post_context":[" }"," }",""]},{"filename":"node_modules/.pnpm/@opentelemetry+context-async-hooks@1.30.1_@opentelemetry+api@1.9.0/node_modules/@opentelemetry/context-async-hooks/build/src/AsyncLocalStorageContextManager.js","module":"@opentelemetry.context-async-hooks.build.src:AsyncLocalStorageContextManager","function":"SentryContextManager.with","lineno":33,"colno":1,"in_app":false,"pre_context":[" with(context, fn, thisArg, ...args) {"," const cb = thisArg == null ? fn : fn.bind(thisArg);"],"context_line":" return this._asyncLocalStorage.run(context, cb, ...args);","post_context":[" }"," enable() {"," return this;"]},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/trace/tracer.js","module":"next.dist.server.lib.trace:tracer","function":"?","lineno":151,"colno":103,"in_app":false,"pre_context":[" }"," const spanId = getSpanId();"," options.attributes = {"," 'next.span_name': spanName,"," 'next.span_type': type,"," ...options.attributes"," };"],"context_line":" return context.with(spanContext.setValue(rootSpanIdKey, spanId), ()=>this.getTracerInstance().startActiveSpan(spanName, options, (span)=>{","post_context":[" const startTime = 'performance' in globalThis && 'measure' in performance ? globalThis.performance.now() : undefined;"," const onCleanup = ()=>{"," rootSpanAttributesStore.delete(spanId);"," if (startTime && process.env.NEXT_OTEL_PERFORMANCE_PREFIX && _constants.LogSpanAllowList.includes(type || '')) {"," performance.measure(`${process.env.NEXT_OTEL_PERFORMANCE_PREFIX}:next-${(type.split('.').pop() || '').replace(/[A-Z] {snip}"," start: startTime,"," end: performance.now()"]},{"filename":"node_modules/.pnpm/@opentelemetry+sdk-trace-base@1.30.1_@opentelemetry+api@1.9.0/node_modules/@opentelemetry/sdk-trace-base/build/esm/Tracer.js","module":"@opentelemetry.sdk-trace-base.build.esm:Tracer","function":"Tracer.startActiveSpan","lineno":120,"colno":27,"in_app":false,"pre_context":[" var span = this.startSpan(name, opts, parentContext);"," var contextWithSpanSet = api.trace.setSpan(parentContext, span);"],"context_line":" return api.context.with(contextWithSpanSet, fn, undefined, span);","post_context":[" };"," /** Returns the active {@link GeneralLimits}. */"," Tracer.prototype.getGeneralLimits = function () {"]},{"filename":"node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/api/context.js","module":"@opentelemetry.api.build.esm.api:context","function":"ContextAPI.with","lineno":88,"colno":1,"in_app":false,"pre_context":[" args[_i - 3] = arguments[_i];"," }"],"context_line":" return (_a = this._getContextManager()).with.apply(_a, __spreadArray([context, fn, thisArg], __read(args), false));","post_context":[" };"," /**"," * Bind a context to a target function or event emitter"]},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"SentryContextManager.with","lineno":1478,"colno":1,"in_app":false,"pre_context":[" setContextOnScope(newCurrentScope, ctx2);",""],"context_line":" return super.with(ctx2, fn, thisArg, ...args);","post_context":[" }"," }",""]},{"filename":"node_modules/.pnpm/@opentelemetry+context-async-hooks@1.30.1_@opentelemetry+api@1.9.0/node_modules/@opentelemetry/context-async-hooks/build/src/AsyncLocalStorageContextManager.js","module":"@opentelemetry.context-async-hooks.build.src:AsyncLocalStorageContextManager","function":"SentryContextManager.with","lineno":33,"colno":1,"in_app":false,"pre_context":[" with(context, fn, thisArg, ...args) {"," const cb = thisArg == null ? fn : fn.bind(thisArg);"],"context_line":" return this._asyncLocalStorage.run(context, cb, ...args);","post_context":[" }"," enable() {"," return this;"]},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/trace/tracer.js","module":"next.dist.server.lib.trace:tracer","function":"?","lineno":169,"colno":36,"in_app":false,"pre_context":[" if (isRootSpan) {"," rootSpanAttributesStore.set(spanId, new Map(Object.entries(options.attributes ?? {})));"," }"," try {"," if (fn.length > 1) {"," return fn(span, (err)=>closeSpanWithError(span, err));"," }"],"context_line":" const result = fn(span);","post_context":[" if ((0, _isthenable.isThenable)(result)) {"," // If there's error make sure it throws"," return result.then((res)=>{"," span.end();"," // Need to pass down the promise result,"," // it could be react stream response with error { error, stream }"," return res;"]},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js","module":"next.dist.compiled.next-server:app-route.runtime.dev","function":"?","lineno":10,"colno":42271,"in_app":false},{"filename":"/home/byk/Projects/getsentry/nextjs-spotlight-test/node_modules/.pnpm/next@15.1.4_@babel+core@7.26.0_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js","module":"next.dist.compiled.next-server:app-route.runtime.dev","function":"AppRouteRouteModule.do","lineno":10,"colno":32883,"in_app":false},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":80,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"sentry-wrapper-module","module":"route.ts","function":"Object.apply","lineno":45,"colno":10,"in_app":true,"pre_context":[" parameterizedRoute: '/api/error',"," headers,"],"context_line":" }).apply(thisArg, args);","post_context":[" },"," });"," }"]},{"filename":"node_modules/.pnpm/@sentry+nextjs@8.50.0_@opentelemetry+core@1.30.1_@opentelemetry+api@1.9.0__@opentelemetry+ins_lobose66tlrxyqsgevxoceuuua/node_modules/@sentry/nextjs/build/cjs/common/wrapRouteHandlerWithSentry.js","module":"@sentry.nextjs.build.cjs.common:wrapRouteHandlerWithSentry","function":"Object.apply","lineno":41,"colno":1,"in_app":false,"pre_context":[" }",""],"context_line":" return core.withIsolationScope(","post_context":[" process.env.NEXT_RUNTIME === 'edge' ? edgeRuntimeIsolationScopeOverride : core.getIsolationScope(),"," () => {"," return core.withScope(async scope => {"]},{"filename":"node_modules/.pnpm/@sentry+core@8.50.0/node_modules/@sentry/core/build/cjs/currentScopes.js","module":"@sentry.core.build.cjs:currentScopes","function":"Object.withIsolationScope","lineno":94,"colno":1,"in_app":false,"pre_context":[" }",""],"context_line":" return acs.withSetIsolationScope(isolationScope, callback);","post_context":[" }",""," return acs.withIsolationScope(rest[0]);"]},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"Object.withSetIsolationScope","lineno":1385,"colno":1,"in_app":false,"pre_context":[" // the OTEL context manager, which uses the presence of this key to determine if it should"," // fork the isolation scope, or not"],"context_line":" return api.context.with(ctx.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, isolationScope), () => {","post_context":[" return callback(getIsolationScope());"," });"," }"]},{"filename":"node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/api/context.js","module":"@opentelemetry.api.build.esm.api:context","function":"ContextAPI.with","lineno":88,"colno":1,"in_app":false,"pre_context":[" args[_i - 3] = arguments[_i];"," }"],"context_line":" return (_a = this._getContextManager()).with.apply(_a, __spreadArray([context, fn, thisArg], __read(args), false));","post_context":[" };"," /**"," * Bind a context to a target function or event emitter"]},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"SentryContextManager.with","lineno":1478,"colno":1,"in_app":false,"pre_context":[" setContextOnScope(newCurrentScope, ctx2);",""],"context_line":" return super.with(ctx2, fn, thisArg, ...args);","post_context":[" }"," }",""]},{"filename":"node_modules/.pnpm/@opentelemetry+context-async-hooks@1.30.1_@opentelemetry+api@1.9.0/node_modules/@opentelemetry/context-async-hooks/build/src/AsyncLocalStorageContextManager.js","module":"@opentelemetry.context-async-hooks.build.src:AsyncLocalStorageContextManager","function":"SentryContextManager.with","lineno":33,"colno":1,"in_app":false,"pre_context":[" with(context, fn, thisArg, ...args) {"," const cb = thisArg == null ? fn : fn.bind(thisArg);"],"context_line":" return this._asyncLocalStorage.run(context, cb, ...args);","post_context":[" }"," enable() {"," return this;"]},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"eval","lineno":1386,"colno":1,"in_app":false,"pre_context":[" // fork the isolation scope, or not"," return api.context.with(ctx.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, isolationScope), () => {"],"context_line":" return callback(getIsolationScope());","post_context":[" });"," }",""]},{"filename":"node_modules/.pnpm/@sentry+nextjs@8.50.0_@opentelemetry+core@1.30.1_@opentelemetry+api@1.9.0__@opentelemetry+ins_lobose66tlrxyqsgevxoceuuua/node_modules/@sentry/nextjs/build/cjs/common/wrapRouteHandlerWithSentry.js","module":"@sentry.nextjs.build.cjs.common:wrapRouteHandlerWithSentry","function":"eval","lineno":44,"colno":1,"in_app":false,"pre_context":[" process.env.NEXT_RUNTIME === 'edge' ? edgeRuntimeIsolationScopeOverride : core.getIsolationScope(),"," () => {"],"context_line":" return core.withScope(async scope => {","post_context":[" scope.setTransactionName(`${method} ${parameterizedRoute}`);",""," if (process.env.NEXT_RUNTIME === 'edge') {"]},{"filename":"node_modules/.pnpm/@sentry+core@8.50.0/node_modules/@sentry/core/build/cjs/currentScopes.js","module":"@sentry.core.build.cjs:currentScopes","function":"Object.withScope","lineno":62,"colno":1,"in_app":false,"pre_context":[" }",""],"context_line":" return acs.withScope(rest[0]);","post_context":[" }",""," /**"]},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"Object.withScope","lineno":1350,"colno":1,"in_app":false,"pre_context":[" // fork the isolation scope, or not"," // as by default, we don't want to fork this, unless triggered explicitly by `withScope`"],"context_line":" return api.context.with(ctx, () => {","post_context":[" return callback(getCurrentScope());"," });"," }"]},{"filename":"node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/api/context.js","module":"@opentelemetry.api.build.esm.api:context","function":"ContextAPI.with","lineno":88,"colno":1,"in_app":false,"pre_context":[" args[_i - 3] = arguments[_i];"," }"],"context_line":" return (_a = this._getContextManager()).with.apply(_a, __spreadArray([context, fn, thisArg], __read(args), false));","post_context":[" };"," /**"," * Bind a context to a target function or event emitter"]},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"SentryContextManager.with","lineno":1478,"colno":1,"in_app":false,"pre_context":[" setContextOnScope(newCurrentScope, ctx2);",""],"context_line":" return super.with(ctx2, fn, thisArg, ...args);","post_context":[" }"," }",""]},{"filename":"node_modules/.pnpm/@opentelemetry+context-async-hooks@1.30.1_@opentelemetry+api@1.9.0/node_modules/@opentelemetry/context-async-hooks/build/src/AsyncLocalStorageContextManager.js","module":"@opentelemetry.context-async-hooks.build.src:AsyncLocalStorageContextManager","function":"SentryContextManager.with","lineno":33,"colno":1,"in_app":false,"pre_context":[" with(context, fn, thisArg, ...args) {"," const cb = thisArg == null ? fn : fn.bind(thisArg);"],"context_line":" return this._asyncLocalStorage.run(context, cb, ...args);","post_context":[" }"," enable() {"," return this;"]},{"filename":"node:internal/async_local_storage/async_hooks","module":"async_hooks","function":"AsyncLocalStorage.run","lineno":91,"colno":14,"in_app":false,"platform":"nodejs"},{"filename":"node_modules/.pnpm/@sentry+opentelemetry@8.50.0_@opentelemetry+api@1.9.0_@opentelemetry+core@1.30.1_@opentelemet_ob5acrtx7evj4imo3abxss3s5a/node_modules/@sentry/opentelemetry/build/cjs/index.js","module":"@sentry.opentelemetry.build.cjs:index","function":"eval","lineno":1351,"colno":1,"in_app":false,"pre_context":[" // as by default, we don't want to fork this, unless triggered explicitly by `withScope`"," return api.context.with(ctx, () => {"],"context_line":" return callback(getCurrentScope());","post_context":[" });"," }",""]},{"filename":"node_modules/.pnpm/@sentry+nextjs@8.50.0_@opentelemetry+core@1.30.1_@opentelemetry+api@1.9.0__@opentelemetry+ins_lobose66tlrxyqsgevxoceuuua/node_modules/@sentry/nextjs/build/cjs/common/wrapRouteHandlerWithSentry.js","module":"@sentry.nextjs.build.cjs.common:wrapRouteHandlerWithSentry","function":"eval","lineno":62,"colno":1,"in_app":false,"pre_context":[" }",""],"context_line":" const response = await core.handleCallbackErrors(","post_context":[" () => originalFunction.apply(thisArg, args),"," error => {"," // Next.js throws errors when calling `redirect()`. We don't wanna report these."]},{"filename":"node_modules/.pnpm/@sentry+core@8.50.0/node_modules/@sentry/core/build/cjs/utils/handleCallbackErrors.js","module":"@sentry.core.build.cjs.utils:handleCallbackErrors","function":"Object.handleCallbackErrors","lineno":26,"colno":1,"in_app":false,"pre_context":[" let maybePromiseResult;"," try {"],"context_line":" maybePromiseResult = fn();","post_context":[" } catch (e) {"," onError(e);"," onFinally();"]},{"filename":"node_modules/.pnpm/@sentry+nextjs@8.50.0_@opentelemetry+core@1.30.1_@opentelemetry+api@1.9.0__@opentelemetry+ins_lobose66tlrxyqsgevxoceuuua/node_modules/@sentry/nextjs/build/cjs/common/wrapRouteHandlerWithSentry.js","module":"@sentry.nextjs.build.cjs.common:wrapRouteHandlerWithSentry","function":"eval","lineno":63,"colno":1,"in_app":false,"pre_context":[""," const response = await core.handleCallbackErrors("],"context_line":" () => originalFunction.apply(thisArg, args),","post_context":[" error => {"," // Next.js throws errors when calling `redirect()`. We don't wanna report these."," if (nextNavigationErrorUtils.isRedirectNavigationError(error)) ; else if (nextNavigationErrorUtils.isNotFoundNavigationError(error)) {"]},{"filename":"app/api/error/route.ts","module":"route.ts","function":"POST$1","lineno":11,"colno":9,"in_app":true,"pre_context":[" export function POST() {"," Sentry.captureMessage(\"About to throw!\");"],"context_line":" throw new Error(\"Error on API route (POST)\");","post_context":[" }",""]}]},"mechanism":{"type":"generic","handled":false}}]},"event_id":"7d644a7be9c14568b3affcfb7f527c34","level":"error","platform":"node","contexts":{"trace":{"parent_span_id":"ea17b53511b4d72d","span_id":"55bf0190aef85f92","trace_id":"66db533aac7bfbb6023647914ccfadc5"},"runtime":{"name":"node","version":"v22.11.0"},"app":{"app_start_time":"2025-01-17T13:30:18.910Z","app_memory":1383841792,"free_memory":7053115392},"os":{"kernel_version":"5.15.167.4-microsoft-standard-WSL2","name":"Ubuntu Linux","version":"22.04"},"device":{"boot_time":"2025-01-15T14:35:16.807Z","arch":"x64","memory_size":16406360064,"free_memory":7053115392,"processor_count":16,"cpu_description":"AMD Ryzen 7 7840U w/ Radeon 780M Graphics","processor_frequency":0},"culture":{"locale":"en-US","timezone":"Europe/London"},"cloud_resource":{}},"server_name":"DESKTOP-83GEP91","timestamp":1737128961.156,"environment":"development","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariablesAsync","Context","ProcessAndThreadBreadcrumbs","Modules","Express","Fastify","Graphql","Mongo","Mongoose","Mysql","Mysql2","Redis","Postgres","Nest","Hapi","Koa","Connect","Tedious","GenericPool","Kafka","Amqplib","LruMemoizer","vercelAI","Http","DistDirRewriteFrames","Spotlight"],"name":"sentry.javascript.nextjs","version":"8.50.0","packages":[{"name":"npm:@sentry/nextjs","version":"8.50.0"},{"name":"npm:@sentry/node","version":"8.50.0"}]},"transaction":"POST /api/error","breadcrumbs":[{"timestamp":1737120631.652,"category":"console","level":"log","message":" \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Ready in 5.9s"},{"timestamp":1737124802.244,"category":"console","level":"log","message":" \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Compiled in 699ms (601 modules)"},{"timestamp":1737124810.846,"category":"console","level":"warning","message":" \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/messages/fast-refresh-reload"}],"request":{"headers":{"host":"localhost:3000","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0","accept":"*/*","accept-language":"en-GB,en-US;q=0.8,en;q=0.5,tr;q=0.3","accept-encoding":"gzip, deflate, br, zstd","referer":"http://localhost:3000/","content-type":"text/plain;charset=UTF-8","content-length":"14","origin":"http://localhost:3000","connection":"keep-alive","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"same-origin","priority":"u=0","pragma":"no-cache","cache-control":"no-cache"},"method":"POST","url":"http://localhost:3000/api/error","cookies":{}},"modules":{}} \ No newline at end of file diff --git a/packages/overlay/_fixtures/send_to_sidecar.cjs b/packages/overlay/_fixtures/send_to_sidecar.cjs index 27c8c185..22a92e8d 100644 --- a/packages/overlay/_fixtures/send_to_sidecar.cjs +++ b/packages/overlay/_fixtures/send_to_sidecar.cjs @@ -8,7 +8,7 @@ const zlib = require('node:zlib'); async function sendData(filePath) { let data; try { - data = await fs.readFile(filePath, 'binary'); + data = await fs.readFile(filePath); } catch (err) { console.error(`Error reading file ${filePath}: ${err}`); return; diff --git a/packages/overlay/src/App.tsx b/packages/overlay/src/App.tsx index a1ad7adf..fd72d8c0 100644 --- a/packages/overlay/src/App.tsx +++ b/packages/overlay/src/App.tsx @@ -10,6 +10,7 @@ import useKeyPress from './lib/useKeyPress'; import { connectToSidecar } from './sidecar'; import type { NotificationCount, SpotlightOverlayOptions } from './types'; import { SPOTLIGHT_OPEN_CLASS_NAME } from './constants'; +import { base64Decode } from './lib/base64'; type AppProps = Omit & Required>; @@ -40,11 +41,18 @@ export default function App({ for (const integration of integrations) { result[integration.name] = []; for (const contentType in initialEvents) { - if (!integration.forwardedContentType?.includes(contentType)) { + const contentTypeBits = contentType.split(';'); + const contentTypeWithoutEncoding = contentTypeBits[0]; + if (!integration.forwardedContentType?.includes(contentTypeWithoutEncoding)) { continue; } - for (const data of initialEvents[contentType]) { - const processedEvent = processEvent(contentType, { data }, integration); + const shouldUseBase64 = contentTypeBits[contentTypeBits.length - 1] === 'base64'; + for (const data of initialEvents[contentTypeWithoutEncoding]) { + const processedEvent = processEvent( + contentTypeWithoutEncoding, + { data: shouldUseBase64 ? base64Decode(data as string) : data }, + integration, + ); if (processedEvent) { result[integration.name].push(processedEvent); } @@ -97,6 +105,7 @@ export default function App({ // `contentType` could for example be "application/x-sentry-envelope" result[contentType] = listener; + result[`${contentType};base64`] = ({ data }) => listener({ data: base64Decode(data as string) }); } return result; }, [integrations]); @@ -110,12 +119,19 @@ export default function App({ const dispatchToContentTypeListener = useCallback( ({ contentType, data }: EventData) => { - const listener = contentTypeListeners[contentType]; + const contentTypeBits = contentType.split(';'); + const contentTypeWithoutEncoding = contentTypeBits[0]; + + const listener = contentTypeListeners[contentTypeWithoutEncoding]; if (!listener) { - log('Got event for unknown content type:', contentType); + log('Got event for unknown content type:', contentTypeWithoutEncoding); return; } - listener({ data }); + if (contentTypeBits[contentTypeBits.length - 1] === 'base64') { + listener({ data: base64Decode(data as string) }); + } else { + listener({ data }); + } }, [contentTypeListeners], ); diff --git a/packages/overlay/src/index.tsx b/packages/overlay/src/index.tsx index 00be5746..cb5eb2e2 100644 --- a/packages/overlay/src/index.tsx +++ b/packages/overlay/src/index.tsx @@ -17,8 +17,8 @@ import { activateLogger, log } from './lib/logger'; import { SpotlightContextProvider } from './lib/useSpotlightContext'; import { React, ReactDOM } from './react-instance'; import type { SpotlightOverlayOptions, WindowWithSpotlight } from './types'; -import { removeURLSuffix } from './utils/removeURLSuffix'; -import initSentry from './utils/instrumentation'; +import { removeURLSuffix } from './lib/removeURLSuffix'; +import initSentry from './lib/instrumentation'; export { default as console } from './integrations/console/index'; export { default as hydrationError } from './integrations/hydration-error/index'; diff --git a/packages/overlay/src/integrations/sentry/components/events/error/Frame.tsx b/packages/overlay/src/integrations/sentry/components/events/error/Frame.tsx index ae5a92c7..92d90d37 100644 --- a/packages/overlay/src/integrations/sentry/components/events/error/Frame.tsx +++ b/packages/overlay/src/integrations/sentry/components/events/error/Frame.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import CopyToClipboard from '../../../../../components/CopyToClipboard'; import OpenInEditor from '../../../../../components/OpenInEditor'; import classNames from '../../../../../lib/classNames'; -import { renderValue } from '../../../../../utils/values'; +import { renderValue } from '../../../../../lib/values'; import type { EventFrame, FrameVars } from '../../../types'; function resolveFilename(filename: string) { diff --git a/packages/overlay/src/integrations/sentry/data/sentryDataCache.spec.ts b/packages/overlay/src/integrations/sentry/data/sentryDataCache.spec.ts index 0e3e0329..9bd01341 100644 --- a/packages/overlay/src/integrations/sentry/data/sentryDataCache.spec.ts +++ b/packages/overlay/src/integrations/sentry/data/sentryDataCache.spec.ts @@ -7,7 +7,7 @@ import fs from 'node:fs'; describe('SentryDataCache', () => { // We need to refactor this to make it actually testable test('Process Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_javascript.txt', 'utf-8'); + const envelope = fs.readFileSync('./_fixtures/envelope_javascript.txt'); const processedEnvelope = processEnvelope({ data: envelope, contentType: 'test' }); expect( sentryDataCache.pushEnvelope({ envelope: processedEnvelope.event, rawEnvelope: processedEnvelope.rawEvent }), diff --git a/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts b/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts index 41e648e3..de18300f 100644 --- a/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts +++ b/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts @@ -3,7 +3,7 @@ import { CONTEXT_LINES_ENDPOINT } from '@spotlightjs/sidecar/constants'; import { DEFAULT_SIDECAR_URL } from '~/constants'; import { RawEventContext } from '~/integrations/integration'; import { log } from '../../../lib/logger'; -import { generate_uuidv4 } from '../../../lib/uuid'; +import { generateUuidv4 } from '../../../lib/uuid'; import { Sdk, SentryErrorEvent, SentryEvent, SentryTransactionEvent, Span, Trace } from '../types'; import { getNativeFetchImplementation } from '../utils/fetch'; import { sdkToPlatform } from '../utils/sdkToPlatform'; @@ -115,7 +115,7 @@ class SentryDataCache { }, ) { if (!event.event_id) { - event.event_id = generate_uuidv4(); + event.event_id = generateUuidv4(); } if (this.eventIds.has(event.event_id)) return; @@ -245,7 +245,7 @@ class SentryDataCache { } subscribe(...args: Subscription) { - const id = generate_uuidv4(); + const id = generateUuidv4(); this.subscribers.set(id, args); return () => { diff --git a/packages/overlay/src/integrations/sentry/index.spec.ts b/packages/overlay/src/integrations/sentry/index.spec.ts index 408c48f8..20102ae0 100644 --- a/packages/overlay/src/integrations/sentry/index.spec.ts +++ b/packages/overlay/src/integrations/sentry/index.spec.ts @@ -7,39 +7,39 @@ import sentryDataCache from './data/sentryDataCache'; describe('Sentry Integration', () => { test('Process Envelope Empty', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_empty.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_empty.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_javascript.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_javascript.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Python Transaction Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_python.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_python.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process PHP Transaction Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_php.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_php.txt'); const processedEnvelope = processEnvelope({ data: envelope, contentType: 'test' }); expect(processedEnvelope).not.toBe(undefined); expect((processedEnvelope.event[1][0][1] as any).type).toEqual('transaction'); }); test('Process Java Transaction Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_java.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_java.txt'); const processedEnvelope = processEnvelope({ data: envelope, contentType: 'test' }); expect(processedEnvelope.event).not.toBe(undefined); expect((processedEnvelope.event[1][0][1] as any).type).toEqual('transaction'); }); test('Process Astro SSR pageload (BE -> FE) trace', () => { - const nodeEnvelope = fs.readFileSync('./_fixtures/envelope_astro_ssr_node.txt', 'binary'); + const nodeEnvelope = fs.readFileSync('./_fixtures/envelope_astro_ssr_node.txt'); const processedNodeEnvelope = processEnvelope({ data: nodeEnvelope, contentType: 'test' }); - const browserEnvelope = fs.readFileSync('./_fixtures/envelope_astro_ssr_browser.txt', 'binary'); + const browserEnvelope = fs.readFileSync('./_fixtures/envelope_astro_ssr_browser.txt'); const processedBrowserEnvelope = processEnvelope({ data: browserEnvelope, contentType: 'test' }); expect(processedNodeEnvelope).not.toBe(undefined); @@ -67,47 +67,47 @@ describe('Sentry Integration', () => { }); test('Process Angular Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_angular.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_angular.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Java Formatted Message Envelope', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_java_formatted_message.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_java_formatted_message.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ Binary Data', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_binary.bin', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_binary.bin'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ Empty Payloads', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_empty_payload.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_empty_payload.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ implicit length, terminated by newline', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_no_len_w_new_line.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_no_len_w_new_line.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ implicit length, terminated by EOF', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_no_len_w_eof.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_no_len_w_eof.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ implicit length, terminated by EOF, empty headers', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_no_len_w_eof_empty_headers.txt', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_no_len_w_eof_empty_headers.txt'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ flutter replay video', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_flutter_replay.bin', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_flutter_replay.bin'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); test('Process Envelope w/ PNG screenshot', () => { - const envelope = fs.readFileSync('./_fixtures/envelope_with_screenshot.bin', 'binary'); + const envelope = fs.readFileSync('./_fixtures/envelope_with_screenshot.bin'); expect(processEnvelope({ data: envelope, contentType: 'test' })).not.toBe(undefined); }); }); diff --git a/packages/overlay/src/integrations/sentry/index.ts b/packages/overlay/src/integrations/sentry/index.ts index 256a46de..68a408ed 100644 --- a/packages/overlay/src/integrations/sentry/index.ts +++ b/packages/overlay/src/integrations/sentry/index.ts @@ -1,5 +1,5 @@ import type { Client, Envelope, EnvelopeItem } from '@sentry/types'; -import { removeURLSuffix } from '~/utils/removeURLSuffix'; +import { removeURLSuffix } from '~/lib/removeURLSuffix'; import { off, on } from '../../lib/eventTarget'; import { log, warn } from '../../lib/logger'; import type { Integration, RawEventContext } from '../integration'; @@ -60,7 +60,7 @@ export default function sentryIntegration(options: SentryIntegrationOptions = {} .getEvents() .filter( e => - e.type != 'transaction' && + e.type !== 'transaction' && (e.contexts?.trace?.trace_id ? sentryDataCache.isTraceLocal(e.contexts?.trace?.trace_id) : null) !== false, ).length; @@ -118,10 +118,7 @@ function parseJSONFromBuffer(data: Uint8Array): object { * @returns parsed envelope */ export function processEnvelope(rawEvent: RawEventContext) { - let buffer = - typeof rawEvent.data === 'string' - ? Uint8Array.from(Array.from(rawEvent.data, c => c.charCodeAt(0))) - : rawEvent.data; + let buffer = typeof rawEvent.data === 'string' ? Uint8Array.from(rawEvent.data, c => c.charCodeAt(0)) : rawEvent.data; function readLine(length?: number) { const cursor = length ?? getLineEnd(buffer); diff --git a/packages/overlay/src/integrations/sentry/utils/traces.spec.ts b/packages/overlay/src/integrations/sentry/utils/traces.spec.ts index cd80c853..3365da09 100644 --- a/packages/overlay/src/integrations/sentry/utils/traces.spec.ts +++ b/packages/overlay/src/integrations/sentry/utils/traces.spec.ts @@ -1,13 +1,13 @@ import { describe, expect, test } from 'vitest'; -import { generate_uuidv4 } from '../../../lib/uuid'; +import { generateUuidv4 } from '../../../lib/uuid'; import type { Span } from '../types'; import { groupSpans } from './traces'; function mockSpan({ duration, ...span }: Partial & { duration?: number } = {}): Span { const defaultTimestamp = new Date().getTime(); return { - trace_id: generate_uuidv4(), - span_id: generate_uuidv4(), + trace_id: generateUuidv4(), + span_id: generateUuidv4(), op: 'unknown', status: 'unknown', start_timestamp: defaultTimestamp, @@ -77,7 +77,7 @@ describe('groupSpans', () => { }); test('missing root transactions as siblings, creates faux parent', () => { - const parent_span_id = generate_uuidv4(); + const parent_span_id = generateUuidv4(); const span1 = mockSpan({ parent_span_id, }); @@ -100,10 +100,10 @@ describe('groupSpans', () => { test('missing root transactions as independent children, creates faux parents', () => { const span1 = mockSpan({ - parent_span_id: generate_uuidv4(), + parent_span_id: generateUuidv4(), }); const span2 = mockSpan({ - parent_span_id: generate_uuidv4(), + parent_span_id: generateUuidv4(), trace_id: span1.trace_id, }); const result = groupSpans([span1, span2]); diff --git a/packages/overlay/src/lib/base64.ts b/packages/overlay/src/lib/base64.ts new file mode 100644 index 00000000..a0dc6fff --- /dev/null +++ b/packages/overlay/src/lib/base64.ts @@ -0,0 +1,5 @@ +export function base64Decode(data: string): Uint8Array { + // TODO: Use Uint8Array.fromBase64 when it becomes available + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64 + return Uint8Array.from(atob(data), c => c.charCodeAt(0)); +} diff --git a/packages/overlay/src/utils/instrumentation.ts b/packages/overlay/src/lib/instrumentation.ts similarity index 100% rename from packages/overlay/src/utils/instrumentation.ts rename to packages/overlay/src/lib/instrumentation.ts diff --git a/packages/overlay/src/utils/removeURLSuffix.ts b/packages/overlay/src/lib/removeURLSuffix.ts similarity index 100% rename from packages/overlay/src/utils/removeURLSuffix.ts rename to packages/overlay/src/lib/removeURLSuffix.ts diff --git a/packages/overlay/src/lib/uuid.ts b/packages/overlay/src/lib/uuid.ts index 4d36a0cc..a8a56bf6 100644 --- a/packages/overlay/src/lib/uuid.ts +++ b/packages/overlay/src/lib/uuid.ts @@ -1,4 +1,4 @@ -export function generate_uuidv4() { +export function generateUuidv4() { let dt = new Date().getTime(); return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { let rnd = Math.random() * 16; //random number in range 0 to 16 diff --git a/packages/overlay/src/utils/values.tsx b/packages/overlay/src/lib/values.tsx similarity index 100% rename from packages/overlay/src/utils/values.tsx rename to packages/overlay/src/lib/values.tsx diff --git a/packages/overlay/src/sidecar.ts b/packages/overlay/src/sidecar.ts index 02ece2fa..665edbc6 100644 --- a/packages/overlay/src/sidecar.ts +++ b/packages/overlay/src/sidecar.ts @@ -4,15 +4,16 @@ import { log } from './lib/logger'; export function connectToSidecar( sidecarUrl: string, // Content Type to listener - contentTypeListeners: Record void>, + contentTypeListeners: Record void>, setOnline: React.Dispatch>, ): () => void { log('Connecting to sidecar at', sidecarUrl); - const sidecarStreamUrl: string = new URL('/stream', sidecarUrl).href; - const source = new EventSource(sidecarStreamUrl); + const sidecarStreamUrl = new URL('/stream', sidecarUrl); + sidecarStreamUrl.searchParams.append('base64', '1'); + const source = new EventSource(sidecarStreamUrl.href); for (const [contentType, listener] of Object.entries(contentTypeListeners)) { - source.addEventListener(contentType, listener); + source.addEventListener(`${contentType}`, listener); } source.addEventListener('open', () => { diff --git a/packages/sidecar/src/main.ts b/packages/sidecar/src/main.ts index bd67f05b..1eaf7574 100644 --- a/packages/sidecar/src/main.ts +++ b/packages/sidecar/src/main.ts @@ -9,7 +9,7 @@ import { contextLinesHandler } from './contextlines.js'; import { activateLogger, enableDebugLogging, logger, type SidecarLogger } from './logger.js'; import { MessageBuffer } from './messageBuffer.js'; -type Payload = [string, string]; +type Payload = [string, Buffer]; type IncomingPayloadCallback = (body: string) => void; @@ -91,6 +91,7 @@ const enableCORS = (handler: RequestHandler): RequestHandler => }, { name: 'enableCORS', op: 'sidecar.http.middleware.cors' }, ); + const streamRequestHandler = (buffer: MessageBuffer, incomingPayload?: IncomingPayloadCallback) => { return function handleStreamRequest( req: IncomingMessage, @@ -111,16 +112,27 @@ const streamRequestHandler = (buffer: MessageBuffer, incomingPayload?: }); res.flushHeaders(); // Send something in the body to trigger the `open` event - // This is mostly for Firefox -- see #376 + // This is mostly for Firefox -- see getsentry/spotlight#376 res.write('\n'); + const useBase64 = searchParams?.get('base64') != null; + const base64Indicator = useBase64 ? ';base64' : ''; + const dataWriter = useBase64 + ? (data: Buffer) => res.write(`data:${data.toString('base64')}\n`) + : (data: Buffer) => { + // The utf-8 encoding here is wrong and is a hack as we are + // sending binary data as utf-8 over SSE which enforces utf-8 + // encoding. This is only for backwards compatibility + for (const line of data.toString('utf-8').split('\n')) { + // This is very important - SSE events are delimited by two newlines + res.write(`data:${line}\n`); + } + }; const sub = buffer.subscribe(([payloadType, data]) => { logger.debug('🕊️ sending to Spotlight'); - res.write(`event:${payloadType}\n`); - // This is very important - SSE events are delimited by two newlines - for (const line of data.split('\n')) { - res.write(`data:${line}\n`); - } + res.write(`event:${payloadType}${base64Indicator}\n`); + dataWriter(data); + // This last \n is important as every message ends with an empty line in SSE res.write('\n'); }); @@ -129,7 +141,7 @@ const streamRequestHandler = (buffer: MessageBuffer, incomingPayload?: res.end(); }); } else if (req.method === 'POST') { - logger.debug(`📩 Received event`); + logger.debug('📩 Received event'); let stream = req; // Check for gzip or deflate encoding and create appropriate stream const encoding = req.headers['content-encoding']; @@ -161,11 +173,7 @@ const streamRequestHandler = (buffer: MessageBuffer, incomingPayload?: if (!contentType) { logger.warn('No content type, skipping payload...'); } else { - // The utf-8 encoding here is wrong and is a hack as we are - // sending binary data as utf-8 over SSE which enforces utf-8 - // encoding. Ideally we'd use base64 encoding for binary data - // but that means a breaking change so leaving this as is for now - buffer.put([contentType, body.toString('utf-8')]); + buffer.put([contentType, body]); } if (process.env.SPOTLIGHT_CAPTURE || incomingPayload) {