From e424a32a3ab679b4c4d169e41b4560249e9c5ef3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 14 Apr 2023 14:03:26 +0200 Subject: [PATCH 01/28] fix(gatsby): Don't crash build when auth token is missing (#7858) --- packages/gatsby/gatsby-node.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/gatsby-node.js b/packages/gatsby/gatsby-node.js index 3ff28720a873..e7e0776fa26f 100644 --- a/packages/gatsby/gatsby-node.js +++ b/packages/gatsby/gatsby-node.js @@ -46,8 +46,15 @@ exports.onCreateWebpackConfig = ({ plugins, getConfig, actions }) => { // Handle sentry-cli configuration errors when the user has not done it not to break // the build. errorHandler(err, invokeErr) { - const { message } = err; + const message = err.message && err.message.toLowerCase() || ''; if (message.includes('organization slug is required') || message.includes('project slug is required')) { + // eslint-disable-next-line no-console + console.log('Sentry [Info]: Not uploading source maps due to missing SENTRY_ORG and SENTRY_PROJECT env variables.') + return; + } + if (message.includes('authentication credentials were not provided')) { + // eslint-disable-next-line no-console + console.warn('Sentry [Warn]: Cannot upload source maps due to missing SENTRY_AUTH_TOKEN env variable.') return; } invokeErr(err); @@ -93,7 +100,7 @@ function injectSentryConfig(config, configFile) { } else { // eslint-disable-next-line no-console console.error( - `Sentry Logger [Error]: Could not inject SDK initialization code into ${prop}, unexpected format: `, + `Sentry [Error]: Could not inject SDK initialization code into ${prop}, unexpected format: `, typeof value, ); } From 7b482bba1847996c09e7b730105f5d40e373f985 Mon Sep 17 00:00:00 2001 From: Jesper Engberg Date: Fri, 14 Apr 2023 17:31:22 +0200 Subject: [PATCH 02/28] Update CHANGELOG.md (#7864) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145ed8130fab..056dd1ffb121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,9 @@ If you want to manually add async context isolation to your application, you can import * as Sentry from '@sentry/node'; const requestHandler = (ctx, next) => { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { Sentry.runWithAsyncContext(async () => { - const hub = Sentry.geCurrentHub(); + const hub = Sentry.getCurrentHub(); hub.configureScope(scope => scope.addEventProcessor(event => From ee60f71d9be3fd82b0fc3a8f9c2efa658cdc8588 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 17 Apr 2023 10:04:35 +0200 Subject: [PATCH 03/28] fix(node): Correct typo in trpc integration transaciton name (#7871) --- packages/node/src/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 14b2abf9a662..3e10c09eb903 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -342,7 +342,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { const sentryTransaction = hub.getScope()?.getTransaction(); if (sentryTransaction) { - sentryTransaction.setName(`trcp/${path}`, 'route'); + sentryTransaction.setName(`trpc/${path}`, 'route'); sentryTransaction.op = 'rpc.server'; const trpcContext: Record = { From 82763b7a64718d739a65df0ff228afd337f84ac0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 17 Apr 2023 10:10:07 +0200 Subject: [PATCH 04/28] fix(nextjs): Mark value injection loader result as uncacheable (#7870) --- packages/nextjs/src/config/loaders/types.ts | 49 ++++++++++++++++--- .../config/loaders/valueInjectionLoader.ts | 3 ++ packages/nextjs/src/config/webpack.ts | 3 +- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/nextjs/src/config/loaders/types.ts b/packages/nextjs/src/config/loaders/types.ts index 14766f077a12..ef3a017be0c7 100644 --- a/packages/nextjs/src/config/loaders/types.ts +++ b/packages/nextjs/src/config/loaders/types.ts @@ -1,27 +1,62 @@ import type webpack from 'webpack'; export type LoaderThis = { - /** Path to the file being loaded */ + /** + * Path to the file being loaded + * + * https://webpack.js.org/api/loaders/#thisresourcepath + */ resourcePath: string; - /** Query at the end of resolved file name ("../some-folder/some-module?foobar" -> resourceQuery: "?foobar") */ + /** + * Query at the end of resolved file name ("../some-folder/some-module?foobar" -> resourceQuery: "?foobar") + * + * https://webpack.js.org/api/loaders/#thisresourcequery + */ resourceQuery: string; - // Function to add outside file used by loader to `watch` process + /** + * Function to add outside file used by loader to `watch` process + * + * https://webpack.js.org/api/loaders/#thisadddependency + */ addDependency: (filepath: string) => void; - // Marks a loader as asynchronous + /** + * Marks a loader result as cacheable. + * + * https://webpack.js.org/api/loaders/#thiscacheable + */ + cacheable: (flag: boolean) => void; + + /** + * Marks a loader as asynchronous + * + * https://webpack.js.org/api/loaders/#thisasync + */ async: webpack.loader.LoaderContext['async']; - // Return errors, code, and sourcemaps from an asynchronous loader + /** + * Return errors, code, and sourcemaps from an asynchronous loader + * + * https://webpack.js.org/api/loaders/#thiscallback + */ callback: webpack.loader.LoaderContext['callback']; } & ( | { - // Loader options in Webpack 4 + /** + * Loader options in Webpack 4 + * + * https://webpack.js.org/api/loaders/#thisquery + */ query: Options; } | { - // Loader options in Webpack 5 + /** + * Loader options in Webpack 5 + * + * https://webpack.js.org/api/loaders/#thisgetoptionsschema + */ getOptions: () => Options; } ); diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index 6a645b0e798d..13f94e7128e4 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -15,6 +15,9 @@ export default function valueInjectionLoader(this: LoaderThis, us // We know one or the other will be defined, depending on the version of webpack being used const { values } = 'getOptions' in this ? this.getOptions() : this.query; + // We do not want to cache injected values across builds + this.cacheable(false); + // Define some global proxy that works on server and on the browser. let injectedCode = 'var _sentryCollisionFreeGlobalObject = typeof window === "undefined" ? global : window;\n'; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 4cce69f5c2c0..13ec394abb34 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -855,7 +855,8 @@ function addValueInjectionLoader( __sentryRewritesTunnelPath__: userSentryOptions.tunnelRoute, // The webpack plugin's release injection breaks the `app` directory so we inject the release manually here instead. - SENTRY_RELEASE: { id: getSentryRelease(buildContext.buildId) }, + // Having a release defined in dev-mode spams releases in Sentry so we only set one in non-dev mode + SENTRY_RELEASE: buildContext.dev ? undefined : { id: getSentryRelease(buildContext.buildId) }, }; const serverValues = { From 7eaf461bcb22e57ea70b2c51336547fffb270a09 Mon Sep 17 00:00:00 2001 From: Jesper Engberg Date: Mon, 17 Apr 2023 10:28:36 +0200 Subject: [PATCH 05/28] chore: Fix bug in changelog about `runWithAsyncContext` (#7873) --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056dd1ffb121..fd32edeee293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ If you want to manually add async context isolation to your application, you can import * as Sentry from '@sentry/node'; const requestHandler = (ctx, next) => { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { Sentry.runWithAsyncContext(async () => { const hub = Sentry.getCurrentHub(); @@ -38,7 +38,11 @@ const requestHandler = (ctx, next) => { ) ); - await next(); + try { + await next(); + } catch (err) { + reject(err); + } resolve(); }); }); From 4c57ca4d6020e5ac1c8a9fce8987a92863afaeef Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 17 Apr 2023 10:29:03 +0200 Subject: [PATCH 06/28] build(sveltekit): Upgrade to Rollup 3 (#7862) --- packages/sveltekit/package.json | 5 +++-- packages/sveltekit/rollup.npm.config.js | 3 +++ rollup/plugins/bundlePlugins.js | 8 ++++++-- yarn.lock | 7 +++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 046a5daa0cb6..871369cde490 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -35,12 +35,13 @@ "@sveltejs/kit": "^1.11.0", "svelte": "^3.44.0", "typescript": "^4.9.3", - "vite": "4.0.0" + "vite": "4.0.0", + "rollup": "^3.20.2" }, "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js --bundleConfigAsCjs", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", diff --git a/packages/sveltekit/rollup.npm.config.js b/packages/sveltekit/rollup.npm.config.js index f9dfe71fd30c..d18b0c102d19 100644 --- a/packages/sveltekit/rollup.npm.config.js +++ b/packages/sveltekit/rollup.npm.config.js @@ -5,6 +5,9 @@ export default makeNPMConfigVariants( entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'], packageSpecificConfig: { external: ['$app/stores'], + output: { + dynamicImportInCjs: true, + } }, }), ); diff --git a/rollup/plugins/bundlePlugins.js b/rollup/plugins/bundlePlugins.js index 697de0c8a80f..f1e3b184cd11 100644 --- a/rollup/plugins/bundlePlugins.js +++ b/rollup/plugins/bundlePlugins.js @@ -14,7 +14,7 @@ import * as path from 'path'; import commonjs from '@rollup/plugin-commonjs'; import deepMerge from 'deepmerge'; import license from 'rollup-plugin-license'; -import resolve from '@rollup/plugin-node-resolve'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; import typescript from '@rollup/plugin-typescript'; @@ -178,5 +178,9 @@ export function makeTSPlugin(jsVersion) { // We don't pass these plugins any options which need to be calculated or changed by us, so no need to wrap them in // another factory function, as they are themselves already factory functions. -export { resolve as makeNodeResolvePlugin }; + +export function makeNodeResolvePlugin() { + return nodeResolve(); +} + export { commonjs as makeCommonJSPlugin }; diff --git a/yarn.lock b/yarn.lock index b0f473085a6a..a20a58bc37d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23620,6 +23620,13 @@ rollup@^3.10.0: optionalDependencies: fsevents "~2.3.2" +rollup@^3.20.2: + version "3.20.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.20.2.tgz#f798c600317f216de2e4ad9f4d9ab30a89b690ff" + integrity sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg== + optionalDependencies: + fsevents "~2.3.2" + rollup@^3.7.0: version "3.18.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.18.0.tgz#2354ba63ba66d6a09c652c3ea0dbcd9dad72bbde" From 4b22708b5bd24bed381e18234f2c76221ec5a254 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 17 Apr 2023 10:50:21 +0200 Subject: [PATCH 07/28] feat(sveltekit): Read adapter output directory from `svelte.config.js` (#7863) Load and read the `svelte.config.js` file. This is necessary to automatically find the output directory that users can specify when setting up the Node adapter. This is a "little" hacky though, because we can't just access the output dir variable. Instead, we actually invoke the adapter (which we can access) and pass a minimal, mostly no-op adapter builder, which will report back the output directory. --- .../sveltekit/src/vite/sentryVitePlugins.ts | 4 +- packages/sveltekit/src/vite/sourceMaps.ts | 38 ++++---- packages/sveltekit/src/vite/svelteConfig.ts | 94 +++++++++++++++++++ .../test/vite/sentrySvelteKitPlugins.test.ts | 16 ++-- .../sveltekit/test/vite/sourceMaps.test.ts | 38 ++++++-- .../sveltekit/test/vite/svelteConfig.test.ts | 69 ++++++++++++++ 6 files changed, 225 insertions(+), 34 deletions(-) create mode 100644 packages/sveltekit/src/vite/svelteConfig.ts create mode 100644 packages/sveltekit/test/vite/svelteConfig.test.ts diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index ce6c4703ea1b..6c932d2f3728 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -37,7 +37,7 @@ const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = { * Sentry adds a few additional properties to your Vite config. * Make sure, it is registered before the SvelteKit plugin. */ -export function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Plugin[] { +export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Promise { const mergedOptions = { ...DEFAULT_PLUGIN_OPTIONS, ...options, @@ -50,7 +50,7 @@ export function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Plu ...mergedOptions.sourceMapsUploadOptions, debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options }; - sentryPlugins.push(makeCustomSentryVitePlugin(pluginOptions)); + sentryPlugins.push(await makeCustomSentryVitePlugin(pluginOptions)); } return sentryPlugins; diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index d72995de6d9e..6505897773be 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -7,14 +7,7 @@ import * as path from 'path'; import * as sorcery from 'sorcery'; import type { Plugin } from 'vite'; -const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = { - // TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options - include: [ - { paths: ['build/client'] }, - { paths: ['build/server/chunks'] }, - { paths: ['build/server'], ignore: ['chunks/**'] }, - ], -}; +import { getAdapterOutputDir, loadSvelteConfig } from './svelteConfig'; // sorcery has no types, so these are some basic type definitions: type Chain = { @@ -45,17 +38,30 @@ type SentryVitePluginOptionsOptionalInclude = Omit { + const svelteConfig = await loadSvelteConfig(); + + const outputDir = await getAdapterOutputDir(svelteConfig); + + const defaultPluginOptions: SentryVitePluginOptions = { + include: [ + { paths: [`${outputDir}/client`] }, + { paths: [`${outputDir}/server/chunks`] }, + { paths: [`${outputDir}/server`], ignore: ['chunks/**'] }, + ], + }; + const mergedOptions = { - ...DEFAULT_PLUGIN_OPTIONS, + ...defaultPluginOptions, ...options, }; + const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions); const { debug } = mergedOptions; const { buildStart, resolveId, transform, renderChunk } = sentryPlugin; - let upload = true; + let isSSRBuild = true; const customPlugin: Plugin = { name: 'sentry-vite-plugin-custom', @@ -88,19 +94,19 @@ export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOpti // `config.build.ssr` is `true` for that first build and `false` in the other ones. // Hence we can use it as a switch to upload source maps only once in main build. if (!config.build.ssr) { - upload = false; + isSSRBuild = false; } }, // We need to start uploading source maps later than in the original plugin - // because SvelteKit is still doing some stuff at closeBundle. + // because SvelteKit is invoking the adapter at closeBundle. + // This means that we need to wait until the adapter is done before we start uploading. closeBundle: async () => { - if (!upload) { + if (!isSSRBuild) { return; } - // TODO: Read the out dir from the node adapter somehow as it can be changed in the adapter options - const outDir = path.resolve(process.cwd(), 'build'); + const outDir = path.resolve(process.cwd(), outputDir); const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js')); // eslint-disable-next-line no-console diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts new file mode 100644 index 000000000000..702e29cb9c3f --- /dev/null +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -0,0 +1,94 @@ +/* eslint-disable @sentry-internal/sdk/no-optional-chaining */ + +import type { Builder, Config } from '@sveltejs/kit'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as url from 'url'; + +/** + * Imports the svelte.config.js file and returns the config object. + * The sveltekit plugins import the config in the same way. + * See: https://github.com/sveltejs/kit/blob/master/packages/kit/src/core/config/index.js#L63 + */ +export async function loadSvelteConfig(): Promise { + // This can only be .js (see https://github.com/sveltejs/kit/pull/4031#issuecomment-1049475388) + const SVELTE_CONFIG_FILE = 'svelte.config.js'; + + const configFile = path.join(process.cwd(), SVELTE_CONFIG_FILE); + + try { + if (!fs.existsSync(configFile)) { + return {}; + } + // @ts-ignore - we explicitly want to import the svelte config here. + const svelteConfigModule = await import(`${url.pathToFileURL(configFile).href}`); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return (svelteConfigModule?.default as Config) || {}; + } catch (e) { + // eslint-disable-next-line no-console + console.warn("[Source Maps Plugin] Couldn't load svelte.config.js:"); + // eslint-disable-next-line no-console + console.log(e); + + return {}; + } +} + +/** + * Attempts to read a custom output directory that can be specidied in the options + * of a SvelteKit adapter. If no custom output directory is specified, the default + * directory is returned. + * + * To get the directory, we have to apply a hack and call the adapter's adapt method + * with a custom adapter `Builder` that only calls the `writeClient` method. + * This method is the first method that is called with the output directory. + * Once we obtained the output directory, we throw an error to exit the adapter. + * + * see: https://github.com/sveltejs/kit/blob/master/packages/adapter-node/index.js#L17 + * + */ +export async function getAdapterOutputDir(svelteConfig: Config): Promise { + // 'build' is the default output dir for the node adapter + let outputDir = 'build'; + + if (!svelteConfig.kit?.adapter) { + return outputDir; + } + + const adapter = svelteConfig.kit.adapter; + + const adapterBuilder: Builder = { + writeClient(dest: string) { + outputDir = dest.replace(/\/client.*/, ''); + throw new Error('We got what we came for, throwing to exit the adapter'); + }, + // @ts-ignore - No need to implement the other methods + log: { + // eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop + minor() {}, + }, + getBuildDirectory: () => '', + // eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop + rimraf: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop + mkdirp: () => {}, + + config: { + kit: { + // @ts-ignore - the builder expects a validated config but for our purpose it's fine to just pass this partial config + paths: { + base: svelteConfig.kit?.paths?.base || '', + }, + }, + }, + }; + + try { + await adapter.adapt(adapterBuilder); + } catch (_) { + // We expect the adapter to throw in writeClient! + } + + return outputDir; +} diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 2ddb1de3b5a0..4479d1d1c0dd 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -4,26 +4,26 @@ import { sentrySvelteKit } from '../../src/vite/sentryVitePlugins'; import * as sourceMaps from '../../src/vite/sourceMaps'; describe('sentryVite()', () => { - it('returns an array of Vite plugins', () => { - const plugins = sentrySvelteKit(); + it('returns an array of Vite plugins', async () => { + const plugins = await sentrySvelteKit(); expect(plugins).toBeInstanceOf(Array); expect(plugins).toHaveLength(1); }); - it('returns the custom sentry source maps plugin by default', () => { - const plugins = sentrySvelteKit(); + it('returns the custom sentry source maps plugin by default', async () => { + const plugins = await sentrySvelteKit(); const plugin = plugins[0]; expect(plugin.name).toEqual('sentry-vite-plugin-custom'); }); - it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", () => { - const plugins = sentrySvelteKit({ autoUploadSourceMaps: false }); + it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", async () => { + const plugins = await sentrySvelteKit({ autoUploadSourceMaps: false }); expect(plugins).toHaveLength(0); }); - it('passes user-specified vite pugin options to the custom sentry source maps plugin', () => { + it('passes user-specified vite pugin options to the custom sentry source maps plugin', async () => { const makePluginSpy = vi.spyOn(sourceMaps, 'makeCustomSentryVitePlugin'); - const plugins = sentrySvelteKit({ + const plugins = await sentrySvelteKit({ debug: true, sourceMapsUploadOptions: { include: ['foo.js'], diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 9db7bd5fb39d..91a1863708b0 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -24,8 +24,8 @@ beforeEach(() => { }); describe('makeCustomSentryVitePlugin()', () => { - it('returns the custom sentry source maps plugin', () => { - const plugin = makeCustomSentryVitePlugin(); + it('returns the custom sentry source maps plugin', async () => { + const plugin = await makeCustomSentryVitePlugin(); expect(plugin.name).toEqual('sentry-vite-plugin-custom'); expect(plugin.apply).toEqual('build'); expect(plugin.enforce).toEqual('post'); @@ -41,8 +41,8 @@ describe('makeCustomSentryVitePlugin()', () => { }); describe('Custom sentry vite plugin', () => { - it('enables source map generation', () => { - const plugin = makeCustomSentryVitePlugin(); + it('enables source map generation', async () => { + const plugin = await makeCustomSentryVitePlugin(); // @ts-ignore this function exists! const sentrifiedConfig = plugin.config({ build: { foo: {} }, test: {} }); expect(sentrifiedConfig).toEqual({ @@ -54,8 +54,8 @@ describe('makeCustomSentryVitePlugin()', () => { }); }); - it('uploads source maps during the SSR build', () => { - const plugin = makeCustomSentryVitePlugin(); + it('uploads source maps during the SSR build', async () => { + const plugin = await makeCustomSentryVitePlugin(); // @ts-ignore this function exists! plugin.configResolved({ build: { ssr: true } }); // @ts-ignore this function exists! @@ -63,8 +63,8 @@ describe('makeCustomSentryVitePlugin()', () => { expect(mockedSentryVitePlugin.writeBundle).toHaveBeenCalledTimes(1); }); - it("doesn't upload source maps during the non-SSR builds", () => { - const plugin = makeCustomSentryVitePlugin(); + it("doesn't upload source maps during the non-SSR builds", async () => { + const plugin = await makeCustomSentryVitePlugin(); // @ts-ignore this function exists! plugin.configResolved({ build: { ssr: false } }); @@ -73,4 +73,26 @@ describe('makeCustomSentryVitePlugin()', () => { expect(mockedSentryVitePlugin.writeBundle).not.toHaveBeenCalled(); }); }); + + it('catches errors while uploading source maps', async () => { + mockedSentryVitePlugin.writeBundle.mockImplementationOnce(() => { + throw new Error('test error'); + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const plugin = await makeCustomSentryVitePlugin(); + + // @ts-ignore this function exists! + expect(plugin.closeBundle).not.toThrow(); + + // @ts-ignore this function exists! + plugin.configResolved({ build: { ssr: true } }); + // @ts-ignore this function exists! + plugin.closeBundle(); + + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to upload source maps')); + expect(consoleLogSpy).toHaveBeenCalled(); + }); }); diff --git a/packages/sveltekit/test/vite/svelteConfig.test.ts b/packages/sveltekit/test/vite/svelteConfig.test.ts new file mode 100644 index 000000000000..73f624c8b1be --- /dev/null +++ b/packages/sveltekit/test/vite/svelteConfig.test.ts @@ -0,0 +1,69 @@ +import { vi } from 'vitest'; + +import { getAdapterOutputDir, loadSvelteConfig } from '../../src/vite/svelteConfig'; + +let existsFile; + +describe('loadSvelteConfig', () => { + vi.mock('fs', () => { + return { + existsSync: () => existsFile, + }; + }); + + vi.mock(`${process.cwd()}/svelte.config.js`, () => { + return { + default: { + kit: { + adapter: {}, + }, + }, + }; + }); + + // url apparently doesn't exist in the test environment, therefore we mock it: + vi.mock('url', () => { + return { + pathToFileURL: path => { + return { + href: path, + }; + }, + }; + }); + + beforeEach(() => { + existsFile = true; + vi.clearAllMocks(); + }); + + it('returns the svelte config', async () => { + const config = await loadSvelteConfig(); + expect(config).toStrictEqual({ + kit: { + adapter: {}, + }, + }); + }); + + it('returns an empty object if svelte.config.js does not exist', async () => { + existsFile = false; + + const config = await loadSvelteConfig(); + expect(config).toStrictEqual({}); + }); +}); + +describe('getAdapterOutputDir', () => { + const mockedAdapter = { + name: 'mocked-adapter', + adapt(builder) { + builder.writeClient('customBuildDir'); + }, + }; + + it('returns the output directory of the adapter', async () => { + const outputDir = await getAdapterOutputDir({ kit: { adapter: mockedAdapter } }); + expect(outputDir).toEqual('customBuildDir'); + }); +}); From dce283137ca787256977d6355e5649f544dfc2c3 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 17 Apr 2023 14:58:22 +0200 Subject: [PATCH 08/28] chore(otel): Use correct glob for yarn clean. (#7876) --- packages/opentelemetry-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 35cb2f43b042..bc0a54860d14 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -45,7 +45,7 @@ "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-node-*.tgz", + "clean": "rimraf build coverage sentry-opentelemetry-node-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", From e3313ad78e347c36fbbadef8842d0377a934e5f5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 17 Apr 2023 17:29:00 +0200 Subject: [PATCH 09/28] fix(tracing): Ensure we use s instead of ms for startTimestamp (#7877) Let's deprecate `timestampWithMs` in a follow up PR. --- packages/angular/src/tracing.ts | 4 ++-- packages/core/src/tracing/idletransaction.ts | 10 +++++----- packages/core/src/tracing/span.ts | 6 +++--- packages/ember/addon/index.ts | 4 ++-- .../instance-initializers/sentry-performance.ts | 12 ++++++------ packages/react/src/profiler.tsx | 8 ++++---- packages/tracing-internal/src/browser/router.ts | 4 ++-- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index c200fbc567cb..b206d7fe429d 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -10,7 +10,7 @@ import { Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; import { getCurrentHub, WINDOW } from '@sentry/browser'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; -import { logger, stripUrlQueryAndFragment, timestampWithMs } from '@sentry/utils'; +import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; import { Subscription } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; @@ -258,7 +258,7 @@ export function TraceMethodDecorator(): MethodDecorator { const originalMethod = descriptor.value; // eslint-disable-next-line @typescript-eslint/no-explicit-any descriptor.value = function (...args: any[]): ReturnType { - const now = timestampWithMs(); + const now = timestampInSeconds(); const activeTransaction = getActiveTransaction(); if (activeTransaction) { activeTransaction.startChild({ diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 2a25fa9b45bf..9f7964a33f3f 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { TransactionContext } from '@sentry/types'; -import { logger, timestampWithMs } from '@sentry/utils'; +import { logger, timestampInSeconds } from '@sentry/utils'; import type { Hub } from '../hub'; import type { Span } from './span'; @@ -46,7 +46,7 @@ export class IdleTransactionSpanRecorder extends SpanRecorder { if (span.spanId !== this.transactionSpanId) { // We patch span.finish() to pop an activity after setting an endTimestamp. span.finish = (endTimestamp?: number) => { - span.endTimestamp = typeof endTimestamp === 'number' ? endTimestamp : timestampWithMs(); + span.endTimestamp = typeof endTimestamp === 'number' ? endTimestamp : timestampInSeconds(); this._popActivity(span.spanId); }; @@ -128,7 +128,7 @@ export class IdleTransaction extends Transaction { } /** {@inheritDoc} */ - public finish(endTimestamp: number = timestampWithMs()): string | undefined { + public finish(endTimestamp: number = timestampInSeconds()): string | undefined { this._finished = true; this.activities = {}; @@ -301,13 +301,13 @@ export class IdleTransaction extends Transaction { } if (Object.keys(this.activities).length === 0) { - const endTimestamp = timestampWithMs(); + const endTimestamp = timestampInSeconds(); if (this._idleTimeoutCanceledPermanently) { this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; this.finish(endTimestamp); } else { // We need to add the timeout here to have the real endtimestamp of the transaction - // Remember timestampWithMs is in seconds, timeout is in ms + // Remember timestampInSeconds is in seconds, timeout is in ms this._restartIdleTimeout(endTimestamp + this._idleTimeout / 1000); } } diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 72f27721bb28..fe9e7aba017c 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -7,7 +7,7 @@ import type { TraceContext, Transaction, } from '@sentry/types'; -import { dropUndefinedKeys, logger, timestampWithMs, uuid4 } from '@sentry/utils'; +import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; /** * Keeps track of finished spans for a given transaction @@ -71,7 +71,7 @@ export class Span implements SpanInterface { /** * Timestamp in seconds when the span was created. */ - public startTimestamp: number = timestampWithMs(); + public startTimestamp: number = timestampInSeconds(); /** * Timestamp in seconds when the span ended. @@ -257,7 +257,7 @@ export class Span implements SpanInterface { } } - this.endTimestamp = typeof endTimestamp === 'number' ? endTimestamp : timestampWithMs(); + this.endTimestamp = typeof endTimestamp === 'number' ? endTimestamp : timestampInSeconds(); } /** diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 55050d43f0d0..cf1f7e2f23b9 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -4,7 +4,7 @@ import { macroCondition, isDevelopingApp, getOwnConfig } from '@embroider/macros import { next } from '@ember/runloop'; import { assert, warn } from '@ember/debug'; import Ember from 'ember'; -import { timestampWithMs, GLOBAL_OBJ } from '@sentry/utils'; +import { timestampInSeconds, GLOBAL_OBJ } from '@sentry/utils'; import { GlobalConfig, OwnConfig } from './types'; function _getSentryInitConfig() { @@ -68,7 +68,7 @@ export const getActiveTransaction = () => { export const instrumentRoutePerformance = (BaseRoute: any) => { const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => { - const startTimestamp = timestampWithMs(); + const startTimestamp = timestampInSeconds(); const result = await fn(...args); const currentTransaction = getActiveTransaction(); diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 35725d503c77..1e575f71fe76 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -6,7 +6,7 @@ import { ExtendedBackburner } from '@sentry/ember/runloop'; import { Span, Transaction } from '@sentry/types'; import { EmberRunQueues } from '@ember/runloop/-private/types'; import { getActiveTransaction } from '..'; -import { browserPerformanceTimeOrigin, GLOBAL_OBJ, timestampWithMs } from '@sentry/utils'; +import { browserPerformanceTimeOrigin, GLOBAL_OBJ, timestampInSeconds } from '@sentry/utils'; import { macroCondition, isTesting, getOwnConfig } from '@embroider/macros'; import { EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; import RouterService from '@ember/routing/router-service'; @@ -182,14 +182,14 @@ function _instrumentEmberRunloop(config: EmberSentryConfig) { if (currentQueueSpan) { currentQueueSpan.finish(); } - currentQueueStart = timestampWithMs(); + currentQueueStart = timestampInSeconds(); instrumentedEmberQueues.forEach(queue => { scheduleOnce(queue, null, () => { scheduleOnce(queue, null, () => { // Process this queue using the end of the previous queue. if (currentQueueStart) { - const now = timestampWithMs(); + const now = timestampInSeconds(); const minQueueDuration = minimumRunloopQueueDuration ?? 5; if ((now - currentQueueStart) * 1000 >= minQueueDuration) { @@ -210,7 +210,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig) { if (!stillActiveTransaction) { return; } - currentQueueStart = timestampWithMs(); + currentQueueStart = timestampInSeconds(); }); }); }); @@ -244,7 +244,7 @@ interface RenderEntries { function processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries) { const info = { payload, - now: timestampWithMs(), + now: timestampInSeconds(), }; beforeEntries[payload.object] = info; } @@ -261,7 +261,7 @@ function processComponentRenderAfter( return; } - const now = timestampWithMs(); + const now = timestampInSeconds(); const componentRenderDuration = now - begin.now; if (componentRenderDuration * 1000 >= minComponentDuration) { diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 643ca818ca75..1c6056aad68b 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -3,7 +3,7 @@ import type { Hub } from '@sentry/browser'; import { getCurrentHub } from '@sentry/browser'; import type { Span, Transaction } from '@sentry/types'; -import { timestampWithMs } from '@sentry/utils'; +import { timestampInSeconds } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -82,7 +82,7 @@ class Profiler extends React.Component { // set as data on the span. We just store the prop keys as the values could be potenially very large. const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]); if (changedProps.length > 0) { - const now = timestampWithMs(); + const now = timestampInSeconds(); this._updateSpan = this._mountSpan.startChild({ data: { changedProps, @@ -114,7 +114,7 @@ class Profiler extends React.Component { // next activity as a child to the component mount activity. this._mountSpan.startChild({ description: `<${name}>`, - endTimestamp: timestampWithMs(), + endTimestamp: timestampInSeconds(), op: REACT_RENDER_OP, startTimestamp: this._mountSpan.endTimestamp, }); @@ -195,7 +195,7 @@ function useProfiler( if (mountSpan && options.hasRenderSpan) { mountSpan.startChild({ description: `<${name}>`, - endTimestamp: timestampWithMs(), + endTimestamp: timestampInSeconds(), op: REACT_RENDER_OP, startTimestamp: mountSpan.endTimestamp, }); diff --git a/packages/tracing-internal/src/browser/router.ts b/packages/tracing-internal/src/browser/router.ts index e4e5f901dd0b..ef3c0b7a6257 100644 --- a/packages/tracing-internal/src/browser/router.ts +++ b/packages/tracing-internal/src/browser/router.ts @@ -22,8 +22,8 @@ export function instrumentRoutingWithDefaults( if (startTransactionOnPageLoad) { activeTransaction = customStartTransaction({ name: WINDOW.location.pathname, - // pageload should always start at timeOrigin - startTimestamp: browserPerformanceTimeOrigin, + // pageload should always start at timeOrigin (and needs to be in s, not ms) + startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, op: 'pageload', metadata: { source: 'url' }, }); From 1b2115481c90b253279a81cae91b322e99c46187 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 17 Apr 2023 17:50:56 +0200 Subject: [PATCH 10/28] ci: Ensure we pass correct token to auto-merge step (#7879) --- .github/workflows/gitflow-sync-develop.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/gitflow-sync-develop.yml b/.github/workflows/gitflow-sync-develop.yml index 374e83aeda65..43cf9374fd2b 100644 --- a/.github/workflows/gitflow-sync-develop.yml +++ b/.github/workflows/gitflow-sync-develop.yml @@ -40,6 +40,8 @@ jobs: - name: Enable automerge for PR run: gh pr merge --merge --auto "1" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/marketplace/actions/auto-approve - name: Auto approve PR From de510117b40fa99d1a701d89be4b76f67bb6d451 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 18 Apr 2023 10:18:19 +0200 Subject: [PATCH 11/28] ref(deprecate): Deprecate `timestampWithMs` (#7878) --- MIGRATION.md | 4 ++++ packages/utils/src/time.ts | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index fd0f4c366eea..53aa05d9207d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,9 @@ # Deprecations in 7.x +## Deprecate `timestampWithMs` export - #7878 + +The `timestampWithMs` util is deprecated in favor of using `timestampInSeconds`. + ## Remove requirement for `@sentry/tracing` package (since 7.46.0) With `7.46.0` you no longer require the `@sentry/tracing` package to use tracing and performance monitoring with the Sentry JavaScript SDKs. The `@sentry/tracing` package will be removed in a future major release, but can still be used in the meantime. diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts index 455b5a391540..1c366f09d952 100644 --- a/packages/utils/src/time.ts +++ b/packages/utils/src/time.ts @@ -121,7 +121,12 @@ export const dateTimestampInSeconds: () => number = dateTimestampSource.nowSecon */ export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind(timestampSource); -// Re-exported with an old name for backwards-compatibility. +/** + * Re-exported with an old name for backwards-compatibility. + * TODO (v8): Remove this + * + * @deprecated Use `timestampInSeconds` instead. + */ export const timestampWithMs = timestampInSeconds; /** From 1dea45ea3288dd1762910d7a6b1fb8ef2cef9d4c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 19 Apr 2023 10:10:29 +0200 Subject: [PATCH 12/28] fix(sveltekit): Use `sentry.properties` file when uploading source maps (#7890) --- packages/sveltekit/src/vite/sourceMaps.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 6505897773be..fdabdf7bdb72 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -42,6 +42,7 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio const svelteConfig = await loadSvelteConfig(); const outputDir = await getAdapterOutputDir(svelteConfig); + const hasSentryProperties = fs.existsSync(path.resolve(process.cwd(), 'sentry.properties')); const defaultPluginOptions: SentryVitePluginOptions = { include: [ @@ -49,6 +50,7 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio { paths: [`${outputDir}/server/chunks`] }, { paths: [`${outputDir}/server`], ignore: ['chunks/**'] }, ], + configFile: hasSentryProperties ? 'sentry.properties' : undefined, }; const mergedOptions = { From e6fb47b336d8867824cd4e097d7ca20ecb07edc5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 19 Apr 2023 11:32:52 +0200 Subject: [PATCH 13/28] ci: Tag issues if last commenter is user (#7880) --- .github/workflows/label-last-commenter.yml | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/label-last-commenter.yml diff --git a/.github/workflows/label-last-commenter.yml b/.github/workflows/label-last-commenter.yml new file mode 100644 index 000000000000..2d938459de78 --- /dev/null +++ b/.github/workflows/label-last-commenter.yml @@ -0,0 +1,31 @@ +name: 'Tag issues with last commenter' + +on: + issue_comment: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + if: ${{ !github.event.issue.pull_request }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Add label if commenter is not member + if: | + github.event.comment.author_association != 'COLLABORATOR' + && github.event.comment.author_association != 'MEMBER' + && github.event.comment.author_association != 'OWNER' + uses: actions-ecosystem/action-add-labels@v1 + with: + labels: 'Waiting for: Team' + + - name: Remove label if commenter is member + if: | + github.event.comment.author_association == 'COLLABORATOR' + || github.event.comment.author_association == 'MEMBER' + || github.event.comment.author_association == 'OWNER' + uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: 'Waiting for: Team' From 57cb2fc9dda7ac73657ef3a1d113c221d64464b7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 19 Apr 2023 11:33:21 +0200 Subject: [PATCH 14/28] feat(browser): Simplify stack parsers (#7897) --- packages/browser/src/stack-parsers.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index e4c66755f232..bb9be84aee64 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -55,7 +55,7 @@ function createFrame(filename: string, func: string, lineno?: number, colno?: nu // Chromium based browsers: Chrome, Brave, new Opera, new Edge const chromeRegex = - /^\s*at (?:(.*\).*?|.*?) ?\((?:address at )?)?(?:async )?((?:file|https?|blob|chrome-extension|address|native|eval|webpack||[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; + /^\s*at (?:(.*\).*?|.*?) ?\((?:address at )?)?(?:async )?((?:|[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; const chromeEvalRegex = /\((\S*)(?::(\d+))(?::(\d+))\)/; const chrome: StackLineParserFn = line => { @@ -91,7 +91,7 @@ export const chromeStackLineParser: StackLineParser = [CHROME_PRIORITY, chrome]; // generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js // We need this specific case for now because we want no other regex to match. const geckoREgex = - /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|safari-extension|safari-web-extension|capacitor)?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; + /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:[-a-z]+)?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; const geckoEvalRegex = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; const gecko: StackLineParserFn = line => { @@ -123,8 +123,7 @@ const gecko: StackLineParserFn = line => { export const geckoStackLineParser: StackLineParser = [GECKO_PRIORITY, gecko]; -const winjsRegex = - /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; +const winjsRegex = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:[-a-z]+):.*?):(\d+)(?::(\d+))?\)?\s*$/i; const winjs: StackLineParserFn = line => { const parts = winjsRegex.exec(line); From 36d6630d88747b7adbb0bc9ce27b016748473e63 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 19 Apr 2023 12:31:50 +0200 Subject: [PATCH 15/28] fix(gatsby): Use `import` for `gatsby-browser.js` instead of `require` (#7889) --- packages/gatsby/.eslintrc.js | 10 ++++++++++ packages/gatsby/gatsby-browser.js | 8 ++++---- packages/gatsby/jest.config.js | 4 ++++ packages/gatsby/package.json | 4 ++-- packages/gatsby/test/integration.test.tsx | 2 +- packages/gatsby/tsconfig.test.json | 5 ++++- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/gatsby/.eslintrc.js b/packages/gatsby/.eslintrc.js index 88a6a96293ee..e4856316be0e 100644 --- a/packages/gatsby/.eslintrc.js +++ b/packages/gatsby/.eslintrc.js @@ -15,6 +15,16 @@ module.exports = { project: ['../../tsconfig.dev.json'], }, }, + { + files: ['./gatsby-browser.js'], + env: { + browser: true, + node: false, + }, + parserOptions: { + sourceType: 'module', + }, + }, ], extends: ['../../.eslintrc.js'], }; diff --git a/packages/gatsby/gatsby-browser.js b/packages/gatsby/gatsby-browser.js index 7c883a74bffc..3eff2faf439f 100644 --- a/packages/gatsby/gatsby-browser.js +++ b/packages/gatsby/gatsby-browser.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ -const Sentry = require('@sentry/gatsby'); +import { init } from '@sentry/gatsby'; -exports.onClientEntry = function (_, pluginParams) { +export function onClientEntry(_, pluginParams) { const isIntialized = isSentryInitialized(); const areOptionsDefined = areSentryOptionsDefined(pluginParams); @@ -24,12 +24,12 @@ exports.onClientEntry = function (_, pluginParams) { return; } - Sentry.init({ + init({ // eslint-disable-next-line no-undef dsn: __SENTRY_DSN__, ...pluginParams, }); -}; +} function isSentryInitialized() { // Although `window` should exist because we're in the browser (where this script diff --git a/packages/gatsby/jest.config.js b/packages/gatsby/jest.config.js index cc7d8162cd59..9a27709769d4 100644 --- a/packages/gatsby/jest.config.js +++ b/packages/gatsby/jest.config.js @@ -4,4 +4,8 @@ module.exports = { ...baseConfig, setupFiles: ['/test/setEnvVars.ts'], testEnvironment: 'jsdom', + transform: { + '^.+\\.js$': 'ts-jest', + ...baseConfig.transform, + }, }; diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 99a27eaccc07..0c404e26ad8a 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -50,10 +50,10 @@ "clean": "rimraf build coverage *.d.ts sentry-gatsby-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", - "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", + "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.{ts,tsx,js}\"", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --format stylish", - "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.{ts,tsx,js}\"", "test": "yarn ts-node scripts/pretest.ts && yarn jest", "test:watch": "yarn ts-node scripts/pretest.ts && yarn jest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" diff --git a/packages/gatsby/test/integration.test.tsx b/packages/gatsby/test/integration.test.tsx index fcad48c60032..2253c9d1ba94 100644 --- a/packages/gatsby/test/integration.test.tsx +++ b/packages/gatsby/test/integration.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react'; import { useEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import * as React from 'react'; -import { TextDecoder,TextEncoder } from 'util'; +import { TextDecoder, TextEncoder } from 'util'; import { onClientEntry } from '../gatsby-browser'; import * as Sentry from '../src'; diff --git a/packages/gatsby/tsconfig.test.json b/packages/gatsby/tsconfig.test.json index 87f6afa06b86..0ac551ca12df 100644 --- a/packages/gatsby/tsconfig.test.json +++ b/packages/gatsby/tsconfig.test.json @@ -5,8 +5,11 @@ "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used - "types": ["node", "jest"] + "types": ["node", "jest"], // other package-specific, test-specific options + + // Needed to parse ESM from gatsby-browser.js + "allowJs": true } } From ef3325092453d682108cfb77218cb3141ff3dd80 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 19 Apr 2023 13:56:55 +0200 Subject: [PATCH 16/28] ref(nextjs): Don't use Sentry Webpack Plugin in dev mode (#7901) --- packages/nextjs/src/config/webpack.ts | 31 ++++++------------- .../webpack/sentryWebpackPlugin.test.ts | 1 - 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 13ec394abb34..e00ac0d6a659 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -285,14 +285,14 @@ export function constructWebpackConfigFunction( // front-end-only problem, and because `sentry-cli` handles sourcemaps more reliably with the comment than // without, the option to use `hidden-source-map` only applies to the client-side build. newConfig.devtool = userSentryOptions.hideSourceMaps && !isServer ? 'hidden-source-map' : 'source-map'; - } - newConfig.plugins = newConfig.plugins || []; - newConfig.plugins.push( - new SentryWebpackPlugin( - getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions, userSentryOptions), - ), - ); + newConfig.plugins = newConfig.plugins || []; + newConfig.plugins.push( + new SentryWebpackPlugin( + getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions, userSentryOptions), + ), + ); + } } return newConfig; @@ -584,7 +584,7 @@ export function getWebpackPluginOptions( userPluginOptions: Partial, userSentryOptions: UserSentryOptions, ): SentryWebpackPluginOptions { - const { buildId, isServer, webpack, config, dev: isDev, dir: projectDir } = buildContext; + const { buildId, isServer, webpack, config, dir: projectDir } = buildContext; const userNextConfig = config as NextConfigObject; const distDirAbsPath = path.resolve(projectDir, userNextConfig.distDir || '.next'); // `.next` is the default directory @@ -628,7 +628,6 @@ export function getWebpackPluginOptions( urlPrefix, entries: [], // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. release: getSentryRelease(buildId), - dryRun: isDev, }); checkWebpackPluginOverrides(defaultPluginOptions, userPluginOptions); @@ -733,7 +732,7 @@ export function getWebpackPluginOptions( /** Check various conditions to decide if we should run the plugin */ function shouldEnableWebpackPlugin(buildContext: BuildContext, userSentryOptions: UserSentryOptions): boolean { - const { isServer, dev: isDev } = buildContext; + const { isServer } = buildContext; const { disableServerWebpackPlugin, disableClientWebpackPlugin } = userSentryOptions; /** Non-negotiable */ @@ -758,18 +757,6 @@ function shouldEnableWebpackPlugin(buildContext: BuildContext, userSentryOptions return !disableClientWebpackPlugin; } - /** Situations where the default is to disable the plugin */ - - // TODO: Are there analogs to Vercel's preveiw and dev modes on other deployment platforms? - - if (isDev || process.env.NODE_ENV === 'development') { - // TODO (v8): Right now in dev we set the plugin to dryrun mode, and our boilerplate includes setting the plugin to - // `silent`, so for the vast majority of users, it's as if the plugin doesn't run at all in dev. Making that - // official is technically a breaking change, though, so we probably should wait until v8. - // return false - } - - // We've passed all of the tests! return true; } diff --git a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts index bc6b8dd458a1..1ec6af7212a1 100644 --- a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts +++ b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts @@ -39,7 +39,6 @@ describe('Sentry webpack plugin config', () => { urlPrefix: '~/_next', // default entries: [], release: 'doGsaREgReaT', // picked up from env - dryRun: false, // based on buildContext.dev being false }), ); }); From 63d19379a11dab6c23ce7d56e7a43574e72bb2f2 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 19 Apr 2023 15:24:09 +0200 Subject: [PATCH 17/28] chore(ci): Add Node 20 to the test matrix (#7904) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae90621dc220..acf25d7b5cc0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -433,7 +433,7 @@ jobs: strategy: fail-fast: false matrix: - node: [8, 10, 12, 14, 16, 18] + node: [8, 10, 12, 14, 16, 18, 20] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v3 @@ -698,7 +698,7 @@ jobs: strategy: fail-fast: false matrix: - node: [10, 12, 14, 16, 18] + node: [10, 12, 14, 16, 18, 20] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v3 From 55b51ef16d211acf348309832287f5dbbfcc6832 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 19 Apr 2023 15:25:33 +0200 Subject: [PATCH 18/28] fix(nextjs): Handle braces in stack frame URLs (#7900) --- packages/browser/src/stack-parsers.ts | 2 +- .../test/unit/tracekit/chromium.test.ts | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index bb9be84aee64..609a6dd1fd51 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -55,7 +55,7 @@ function createFrame(filename: string, func: string, lineno?: number, colno?: nu // Chromium based browsers: Chrome, Brave, new Opera, new Edge const chromeRegex = - /^\s*at (?:(.*\).*?|.*?) ?\((?:address at )?)?(?:async )?((?:|[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; + /^\s*at (?:(.+?\)(?: \[.+\])?|.*?) ?\((?:address at )?)?(?:async )?((?:|[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; const chromeEvalRegex = /\((\S*)(?::(\d+))(?::(\d+))\)/; const chrome: StackLineParserFn = line => { diff --git a/packages/browser/test/unit/tracekit/chromium.test.ts b/packages/browser/test/unit/tracekit/chromium.test.ts index b0df582ebeef..56b5844711e7 100644 --- a/packages/browser/test/unit/tracekit/chromium.test.ts +++ b/packages/browser/test/unit/tracekit/chromium.test.ts @@ -573,6 +573,41 @@ describe('Tracekit - Chrome Tests', () => { }); }); + it('handles braces in urls', () => { + const CHROME_BRACES_URL = { + message: 'bad', + name: 'Error', + stack: `Error: bad + at something (http://localhost:5000/(some)/(thing)/index.html:20:16) + at more (http://localhost:5000/(some)/(thing)/index.html:25:7)`, + }; + + const ex = exceptionFromError(parser, CHROME_BRACES_URL); + + expect(ex).toEqual({ + value: 'bad', + type: 'Error', + stacktrace: { + frames: [ + { + filename: 'http://localhost:5000/(some)/(thing)/index.html', + function: 'more', + lineno: 25, + colno: 7, + in_app: true, + }, + { + filename: 'http://localhost:5000/(some)/(thing)/index.html', + function: 'something', + lineno: 20, + colno: 16, + in_app: true, + }, + ], + }, + }); + }); + it('should drop frames that are over 1kb', () => { const LONG_STR = 'A'.repeat(1040); From 2e64ef7606bc7250e6d5b8e145e5423ece08c5d5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 19 Apr 2023 15:33:31 +0200 Subject: [PATCH 19/28] docs(sveltekit): Improve setup instructions (#7903) --- packages/sveltekit/README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md index cdc3e0e9582a..21393e19c4e7 100644 --- a/packages/sveltekit/README.md +++ b/packages/sveltekit/README.md @@ -55,11 +55,12 @@ Although the SDK is not yet stable, you're more than welcome to give it a try an The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.dev/docs/hooks) to capture error and performance data. -1. If you don't already have a `hooks.client.(js|ts)` file, create a new one. +1. If you don't already have a client hooks file, create a new one in `src/hooks.client.(js|ts)`. 2. On the top of your `hooks.client.(js|ts)` file, initialize the Sentry SDK: ```javascript + // hooks.client.(js|ts) import * as Sentry from '@sentry/sveltekit'; Sentry.init({ @@ -75,6 +76,7 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d 3. Add our `handleErrorWithSentry` function to the `handleError` hook: ```javascript + // hooks.client.(js|ts) import { handleErrorWithSentry } from '@sentry/sveltekit'; const myErrorHandler = (({ error, event }) => { @@ -88,11 +90,12 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d ### 3. Server-side Setup -1. If you don't already have a `hooks.server.(js|ts)` file, create a new one. +1. If you don't already have a server hooks file, create a new one in `src/hooks.server.(js|ts)`. 2. On the top of your `hooks.server.(js|ts)` file, initialize the Sentry SDK: ```javascript + // hooks.server.(js|ts) import * as Sentry from '@sentry/sveltekit'; Sentry.init({ @@ -104,6 +107,7 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d 3. Add our `handleErrorWithSentry` function to the `handleError` hook: ```javascript + // hooks.server.(js|ts) import { handleErrorWithSentry } from '@sentry/sveltekit'; const myErrorHandler = (({ error, event }) => { @@ -118,6 +122,7 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d 4. Add our request handler to the `handle` hook in `hooks.server.ts`: ```javascript + // hooks.server.(js|ts) import { sentryHandle } from '@sentry/sveltekit'; export const handle = sentryHandle; @@ -131,6 +136,7 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d 5. To catch errors and performance data in your universal `load` functions (e.g. in `+page.(js|ts)`), wrap our `wrapLoadWithSentry` function around your load code: ```javascript + // +page.(js|ts) import { wrapLoadWithSentry } from '@sentry/sveltekit'; export const load = wrapLoadWithSentry((event) => { @@ -141,6 +147,7 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d 6. To catch errors and performance data in your server `load` functions (e.g. in `+page.server.(js|ts)`), wrap our `wrapServerLoadWithSentry` function around your load code: ```javascript + // +page.server.(js|ts) import { wrapServerLoadWithSentry } from '@sentry/sveltekit'; export const load = wrapServerLoadWithSentry((event) => { @@ -154,6 +161,7 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d Make sure that it is added before the `sveltekit` plugin: ```javascript + // vite.config.(js|ts) import { sveltekit } from '@sveltejs/kit/vite'; import { sentrySvelteKit } from '@sentry/sveltekit'; @@ -178,7 +186,7 @@ can either set them as env variables (for example in a `.env` file): Or you can pass them in whatever form you prefer to `sentrySvelteKit`: ```js -// vite.config.js +// vite.config.(js|ts) import { sveltekit } from '@sveltejs/kit/vite'; import { sentrySvelteKit } from '@sentry/sveltekit'; @@ -204,7 +212,7 @@ Under `sourceMapsUploadOptions`, you can also specify all additional options sup This might be useful if you're using adapters other than the Node adapter or have a more customized build setup. ```js -// vite.config.js +// vite.config.(js|ts) import { sveltekit } from '@sveltejs/kit/vite'; import { sentrySvelteKit } from '@sentry/sveltekit'; @@ -233,7 +241,7 @@ export default { If you don't want to upload source maps automatically, you can disable it as follows: ```js -// vite.config.js +// vite.config.(js|ts) import { sveltekit } from '@sveltejs/kit/vite'; import { sentrySvelteKit } from '@sentry/sveltekit'; From 3040a6b08af2181025aec1b7342e4432fb364cb1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 19 Apr 2023 15:34:12 +0200 Subject: [PATCH 20/28] fix(sveltekit): Detect sentry release before creating the Vite plugins (#7902) --- packages/sveltekit/src/vite/sourceMaps.ts | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index fdabdf7bdb72..88a4c3424719 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -1,5 +1,8 @@ +import { getSentryRelease } from '@sentry/node'; +import { uuid4 } from '@sentry/utils'; import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; import { sentryVitePlugin } from '@sentry/vite-plugin'; +import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; // @ts-ignore -sorcery has no types :( @@ -22,6 +25,10 @@ type SentryVitePluginOptionsOptionalInclude = Omit Date: Wed, 19 Apr 2023 16:03:37 +0200 Subject: [PATCH 21/28] fix(sveltekit): Avoid capturing "Not Found" errors in server `handleError` wrapper (#7898) Incoming page load requests on the server side for unknown/invalid routes will throw a `Error: Not found: /unknown/route` error in the server `handleError` hook. In our wrapper, we don't want to catch these errors (similarly to 404 errors in the `wrapLoadWithSentry` wrapper). --- packages/sveltekit/src/server/handleError.ts | 26 +++++++++++++++++++ .../sveltekit/test/server/handleError.test.ts | 17 ++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/sveltekit/src/server/handleError.ts b/packages/sveltekit/src/server/handleError.ts index 4d886fc80224..022c1c814930 100644 --- a/packages/sveltekit/src/server/handleError.ts +++ b/packages/sveltekit/src/server/handleError.ts @@ -21,6 +21,10 @@ function defaultErrorHandler({ error }: Parameters[0]): Retur */ export function handleErrorWithSentry(handleError: HandleServerError = defaultErrorHandler): HandleServerError { return (input: { error: unknown; event: RequestEvent }): ReturnType => { + if (isNotFoundError(input)) { + return handleError(input); + } + captureException(input.error, scope => { scope.addEventProcessor(event => { addExceptionMechanism(event, { @@ -35,3 +39,25 @@ export function handleErrorWithSentry(handleError: HandleServerError = defaultEr return handleError(input); }; } + +/** + * When a page request fails because the page is not found, SvelteKit throws a "Not found" error. + * In the error handler here, we can't access the response yet (which we do in the load instrumentation), + * so we have to check if the error is a "Not found" error by checking if the route id is missing and + * by checking the error message on top of the raw stack trace. + */ +function isNotFoundError(input: { error: unknown; event: RequestEvent }): boolean { + const { error, event } = input; + + const hasNoRouteId = !event.route || !event.route.id; + + const rawStack: string = + (error != null && + typeof error === 'object' && + 'stack' in error && + typeof error.stack === 'string' && + error.stack) || + ''; + + return hasNoRouteId && rawStack.startsWith('Error: Not found:'); +} diff --git a/packages/sveltekit/test/server/handleError.test.ts b/packages/sveltekit/test/server/handleError.test.ts index e44d5553f928..12ecb83b44e6 100644 --- a/packages/sveltekit/test/server/handleError.test.ts +++ b/packages/sveltekit/test/server/handleError.test.ts @@ -47,6 +47,23 @@ describe('handleError', () => { mockScope = new Scope(); }); + it('doesn\'t capture "Not found" errors for incorrect navigations', async () => { + const wrappedHandleError = handleErrorWithSentry(); + const mockError = new Error('Not found: /asdf/123'); + const mockEvent = { + url: new URL('https://myDomain.com/asdf/123'), + route: { id: null }, // <-- this is what SvelteKit puts in the event when the page is not found + // ... + } as RequestEvent; + + const returnVal = await wrappedHandleError({ error: mockError, event: mockEvent }); + + expect(returnVal).not.toBeDefined(); + expect(mockCaptureException).toHaveBeenCalledTimes(0); + expect(mockAddExceptionMechanism).toHaveBeenCalledTimes(0); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + }); + describe('calls captureException', () => { it('invokes the default handler if no handleError func is provided', async () => { const wrappedHandleError = handleErrorWithSentry(); From b4121cb2d6b3a6fee5e0ca1649de78a7dfb2a998 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 19 Apr 2023 16:15:05 +0200 Subject: [PATCH 22/28] chore(ci): Skip SvelteKit tests in browser CI (#7905) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e02a3f547df..8ebd0541f657 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "postpublish": "lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore @sentry-internal/* test", "test:unit": "lerna run --ignore @sentry-internal/* test:unit", - "test-ci-browser": "lerna run test --ignore \"@sentry/{node,opentelemetry-node,serverless,nextjs,remix,gatsby}\" --ignore @sentry-internal/*", + "test-ci-browser": "lerna run test --ignore \"@sentry/{node,opentelemetry-node,serverless,nextjs,remix,gatsby,sveltekit}\" --ignore @sentry-internal/*", "test-ci-node": "ts-node ./scripts/node-unit-tests.ts", "test:update-snapshots": "lerna run test:update-snapshots", "yalc:publish": "lerna run yalc:publish" From 7896c682b1104ee1dec6d9ac409793ae0005e4d9 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 19 Apr 2023 13:24:51 -0400 Subject: [PATCH 23/28] fix(node): reduce deepReadDirSync runtime complexity (#7910) --- packages/node/src/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts index 87fb8f6157e3..0ff47acf92c7 100644 --- a/packages/node/src/utils.ts +++ b/packages/node/src/utils.ts @@ -27,10 +27,11 @@ export function deepReadDirSync(targetDir: string): string[] { const itemAbsPath = path.join(currentDirAbsPath, itemName); if (fs.statSync(itemAbsPath).isDirectory()) { - return [...absPaths, ...deepReadCurrentDir(itemAbsPath)]; + return absPaths.concat(deepReadCurrentDir(itemAbsPath)); } - return [...absPaths, itemAbsPath]; + absPaths.push(itemAbsPath); + return absPaths; }, []); }; From db013df383ef3ccb2665a2452fd4eeaba65c3e03 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Apr 2023 11:13:06 +0200 Subject: [PATCH 24/28] feat(node): Add monitor upsert types (#7914) --- packages/node/test/checkin.test.ts | 104 +++++++++++++++++++++++------ packages/types/src/checkin.ts | 26 ++++++++ 2 files changed, 111 insertions(+), 19 deletions(-) diff --git a/packages/node/test/checkin.test.ts b/packages/node/test/checkin.test.ts index da082207b00d..4bd1003097dc 100644 --- a/packages/node/test/checkin.test.ts +++ b/packages/node/test/checkin.test.ts @@ -1,7 +1,9 @@ +import type { CheckIn } from '@sentry/types'; + import { createCheckInEnvelope } from '../src/checkin'; -describe('userFeedback', () => { - test('creates user feedback envelope header', () => { +describe('CheckIn', () => { + test('creates a check in envelope header', () => { const envelope = createCheckInEnvelope( { check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', @@ -32,29 +34,93 @@ describe('userFeedback', () => { }); }); - test('creates user feedback envelope item', () => { - const envelope = createCheckInEnvelope({ - check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', - monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', - status: 'ok', - duration: 10.0, - release: '1.0.0', - environment: 'production', - }); + test.each([ + [ + 'no monitor config', + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'ok', + duration: 10.0, + release: '1.0.0', + environment: 'production', + } as CheckIn, + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'ok', + duration: 10.0, + release: '1.0.0', + environment: 'production', + }, + ], + [ + 'crontab monitor config', + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'in_progress', + monitor_config: { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkin_margin: 5, + max_runtime: 30, + timezone: 'America/Los_Angeles', + }, + } as CheckIn, + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'in_progress', + monitor_config: { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkin_margin: 5, + max_runtime: 30, + timezone: 'America/Los_Angeles', + }, + }, + ], + [ + 'interval monitor config', + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'in_progress', + monitor_config: { + schedule: { + type: 'interval', + value: 1234, + unit: 'minute', + }, + }, + } as CheckIn, + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'in_progress', + monitor_config: { + schedule: { + type: 'interval', + value: 1234, + unit: 'minute', + }, + }, + }, + ], + ])('creates a check in envelope header with %s', (_, checkIn, envelopeItem) => { + const envelope = createCheckInEnvelope(checkIn); expect(envelope[1]).toEqual([ [ { type: 'check_in', }, - { - check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', - monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', - status: 'ok', - duration: 10.0, - release: '1.0.0', - environment: 'production', - }, + envelopeItem, ], ]); }); diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index 071afce9640d..a31a5632417c 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -1,3 +1,17 @@ +interface CrontabSchedule { + type: 'crontab'; + // The crontab schedule string, e.g. 0 * * * *. + value: string; +} + +interface IntervalSchedule { + type: 'interval'; + value: number; + unit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute'; +} + +type MonitorSchedule = CrontabSchedule | IntervalSchedule; + // https://develop.sentry.dev/sdk/check-ins/ export interface CheckIn { // Check-In ID (unique and client generated). @@ -10,4 +24,16 @@ export interface CheckIn { duration?: number; release?: string; environment?: string; + monitor_config?: { + schedule: MonitorSchedule; + // The allowed allowed margin of minutes after the expected check-in time that + // the monitor will not be considered missed for. + checkin_margin?: number; + // The allowed allowed duration in minutes that the monitor may be `in_progress` + // for before being considered failed due to timeout. + max_runtime?: number; + // A tz database string representing the timezone which the monitor's execution schedule is in. + // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone?: string; + }; } From 100369e6a52ecf435709ab70bf53d1018ce2d793 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Apr 2023 11:27:07 +0200 Subject: [PATCH 25/28] feat(replay): Truncate network bodies to max size (#7875) --------- Co-authored-by: Abhijeet Prasad --- packages/replay/src/constants.ts | 4 +- .../src/coreHandlers/util/fetchUtils.ts | 29 +- .../src/coreHandlers/util/networkUtils.ts | 55 +++- .../replay/src/coreHandlers/util/xhrUtils.ts | 5 +- packages/replay/src/types.ts | 9 +- .../src/util/truncateJson/completeJson.ts | 125 +++++++++ .../replay/src/util/truncateJson/constants.ts | 23 ++ .../src/util/truncateJson/evaluateJson.ts | 264 ++++++++++++++++++ .../replay/src/util/truncateJson/fixJson.ts | 14 + .../test/fixtures/fixJson/1_completeJson.json | 1 + .../fixtures/fixJson/1_incompleteJson.txt | 1 + .../test/fixtures/fixJson/2_completeJson.json | 22 ++ .../fixtures/fixJson/2_incompleteJson.txt | 22 ++ .../handleNetworkBreadcrumbs.test.ts | 173 +++++++++++- .../coreHandlers/util/networkUtils.test.ts | 127 ++++++++- .../util/truncateJson/fixJson.test.ts | 82 ++++++ 16 files changed, 914 insertions(+), 42 deletions(-) create mode 100644 packages/replay/src/util/truncateJson/completeJson.ts create mode 100644 packages/replay/src/util/truncateJson/constants.ts create mode 100644 packages/replay/src/util/truncateJson/evaluateJson.ts create mode 100644 packages/replay/src/util/truncateJson/fixJson.ts create mode 100644 packages/replay/test/fixtures/fixJson/1_completeJson.json create mode 100644 packages/replay/test/fixtures/fixJson/1_incompleteJson.txt create mode 100644 packages/replay/test/fixtures/fixJson/2_completeJson.json create mode 100644 packages/replay/test/fixtures/fixJson/2_incompleteJson.txt create mode 100644 packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index 57c30fdad662..68ff5fc481ff 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -29,5 +29,5 @@ export const ERROR_CHECKOUT_TIME = 60_000; export const RETRY_BASE_INTERVAL = 5000; export const RETRY_MAX_COUNT = 3; -/* The max (uncompressed) size in bytes of a network body. Any body larger than this will be dropped. */ -export const NETWORK_BODY_MAX_SIZE = 300_000; +/* The max (uncompressed) size in bytes of a network body. Any body larger than this will be truncated. */ +export const NETWORK_BODY_MAX_SIZE = 150_000; diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index 1946efd44f07..b2f7a1ac72d7 100644 --- a/packages/replay/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay/src/coreHandlers/util/fetchUtils.ts @@ -3,7 +3,6 @@ import { logger } from '@sentry/utils'; import type { FetchHint, - NetworkBody, ReplayContainer, ReplayNetworkOptions, ReplayNetworkRequestData, @@ -15,7 +14,6 @@ import { getAllowedHeaders, getBodySize, getBodyString, - getNetworkBody, makeNetworkReplayBreadcrumb, parseContentLengthHeader, } from './networkUtils'; @@ -112,8 +110,8 @@ function _getRequestInfo( // We only want to transmit string or string-like bodies const requestBody = _getFetchRequestArgBody(input); - const body = getNetworkBody(getBodyString(requestBody)); - return buildNetworkRequestOrResponse(headers, requestBodySize, body); + const bodyStr = getBodyString(requestBody); + return buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr); } async function _getResponseInfo( @@ -137,7 +135,7 @@ async function _getResponseInfo( try { // We have to clone this, as the body can only be read once const res = response.clone(); - const { body, bodyText } = await _parseFetchBody(res); + const bodyText = await _parseFetchBody(res); const size = bodyText && bodyText.length && responseBodySize === undefined @@ -145,7 +143,7 @@ async function _getResponseInfo( : responseBodySize; if (captureBodies) { - return buildNetworkRequestOrResponse(headers, size, body); + return buildNetworkRequestOrResponse(headers, size, bodyText); } return buildNetworkRequestOrResponse(headers, size, undefined); @@ -155,25 +153,12 @@ async function _getResponseInfo( } } -async function _parseFetchBody( - response: Response, -): Promise<{ body?: NetworkBody | undefined; bodyText?: string | undefined }> { - let bodyText: string; - +async function _parseFetchBody(response: Response): Promise { try { - bodyText = await response.text(); + return await response.text(); } catch { - return {}; - } - - try { - const body = JSON.parse(bodyText); - return { body, bodyText }; - } catch { - // just send bodyText + return undefined; } - - return { bodyText, body: bodyText }; } function _getFetchRequestArgBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefined { diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 498fb0960c30..6ff4a6ce27d9 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -4,11 +4,13 @@ import { dropUndefinedKeys } from '@sentry/utils'; import { NETWORK_BODY_MAX_SIZE } from '../../constants'; import type { NetworkBody, + NetworkMetaWarning, NetworkRequestData, ReplayNetworkRequestData, ReplayNetworkRequestOrResponse, ReplayPerformanceEntry, } from '../../types'; +import { fixJson } from '../../util/truncateJson/fixJson'; /** Get the size of a body. */ export function getBodySize( @@ -122,7 +124,7 @@ export function getNetworkBody(bodyText: string | undefined): NetworkBody | unde export function buildNetworkRequestOrResponse( headers: Record, bodySize: number | undefined, - body: NetworkBody | undefined, + body: string | undefined, ): ReplayNetworkRequestOrResponse | undefined { if (!bodySize && Object.keys(headers).length === 0) { return undefined; @@ -146,11 +148,11 @@ export function buildNetworkRequestOrResponse( size: bodySize, }; - if (bodySize < NETWORK_BODY_MAX_SIZE) { - info.body = body; - } else { + const { body: normalizedBody, warnings } = normalizeNetworkBody(body); + info.body = normalizedBody; + if (warnings.length > 0) { info._meta = { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings, }; } @@ -175,3 +177,46 @@ function _serializeFormData(formData: FormData): string { // @ts-ignore passing FormData to URLSearchParams actually works return new URLSearchParams(formData).toString(); } + +function normalizeNetworkBody(body: string | undefined): { + body: NetworkBody | undefined; + warnings: NetworkMetaWarning[]; +} { + if (!body || typeof body !== 'string') { + return { + body, + warnings: [], + }; + } + + const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; + + if (_strIsProbablyJson(body)) { + try { + const json = exceedsSizeLimit ? fixJson(body.slice(0, NETWORK_BODY_MAX_SIZE)) : body; + const normalizedBody = JSON.parse(json); + return { + body: normalizedBody, + warnings: exceedsSizeLimit ? ['JSON_TRUNCATED'] : [], + }; + } catch { + return { + body, + warnings: ['INVALID_JSON'], + }; + } + } + + return { + body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, + warnings: exceedsSizeLimit ? ['TEXT_TRUNCATED'] : [], + }; +} + +function _strIsProbablyJson(str: string): boolean { + const first = str[0]; + const last = str[str.length - 1]; + + // Simple check: If this does not start & end with {} or [], it's not JSON + return (first === '[' && last === ']') || (first === '{' && last === '}'); +} diff --git a/packages/replay/src/coreHandlers/util/xhrUtils.ts b/packages/replay/src/coreHandlers/util/xhrUtils.ts index b39148592476..b241bd945771 100644 --- a/packages/replay/src/coreHandlers/util/xhrUtils.ts +++ b/packages/replay/src/coreHandlers/util/xhrUtils.ts @@ -8,7 +8,6 @@ import { getAllowedHeaders, getBodySize, getBodyString, - getNetworkBody, makeNetworkReplayBreadcrumb, parseContentLengthHeader, } from './networkUtils'; @@ -84,12 +83,12 @@ function _prepareXhrData( const request = buildNetworkRequestOrResponse( requestHeaders, requestBodySize, - options.captureBodies ? getNetworkBody(getBodyString(input)) : undefined, + options.captureBodies ? getBodyString(input) : undefined, ); const response = buildNetworkRequestOrResponse( responseHeaders, responseBodySize, - options.captureBodies ? getNetworkBody(hint.xhr.responseText) : undefined, + options.captureBodies ? hint.xhr.responseText : undefined, ); return { diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 98b7ebc4063a..e9faa29b9d6d 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -512,12 +512,15 @@ export type FetchHint = FetchBreadcrumbHint & { response: Response; }; -export type NetworkBody = Record | string; +type JsonObject = Record; +type JsonArray = unknown[]; -type NetworkMetaError = 'MAX_BODY_SIZE_EXCEEDED'; +export type NetworkBody = JsonObject | JsonArray | string; + +export type NetworkMetaWarning = 'JSON_TRUNCATED' | 'TEXT_TRUNCATED' | 'INVALID_JSON'; interface NetworkMeta { - errors?: NetworkMetaError[]; + warnings?: NetworkMetaWarning[]; } export interface ReplayNetworkRequestOrResponse { diff --git a/packages/replay/src/util/truncateJson/completeJson.ts b/packages/replay/src/util/truncateJson/completeJson.ts new file mode 100644 index 000000000000..3e7be2f38a13 --- /dev/null +++ b/packages/replay/src/util/truncateJson/completeJson.ts @@ -0,0 +1,125 @@ +import type { JsonToken } from './constants'; +import { + ARR, + ARR_VAL, + ARR_VAL_COMPLETED, + ARR_VAL_STR, + OBJ, + OBJ_KEY, + OBJ_KEY_STR, + OBJ_VAL, + OBJ_VAL_COMPLETED, + OBJ_VAL_STR, +} from './constants'; + +const ALLOWED_PRIMITIVES = ['true', 'false', 'null']; + +/** + * Complete an incomplete JSON string. + * This will ensure that the last element always has a `"~~"` to indicate it was truncated. + * For example, `[1,2,` will be completed to `[1,2,"~~"]` + * and `{"aa":"b` will be completed to `{"aa":"b~~"}` + */ +export function completeJson(incompleteJson: string, stack: JsonToken[]): string { + if (!stack.length) { + return incompleteJson; + } + + let json = incompleteJson; + + // Most checks are only needed for the last step in the stack + const lastPos = stack.length - 1; + const lastStep = stack[lastPos]; + + json = _fixLastStep(json, lastStep); + + // Complete remaining steps - just add closing brackets + for (let i = lastPos; i >= 0; i--) { + const step = stack[i]; + + switch (step) { + case OBJ: + json = `${json}}`; + break; + case ARR: + json = `${json}]`; + break; + } + } + + return json; +} + +function _fixLastStep(json: string, lastStep: JsonToken): string { + switch (lastStep) { + // Object cases + case OBJ: + return `${json}"~~":"~~"`; + case OBJ_KEY: + return `${json}:"~~"`; + case OBJ_KEY_STR: + return `${json}~~":"~~"`; + case OBJ_VAL: + return _maybeFixIncompleteObjValue(json); + case OBJ_VAL_STR: + return `${json}~~"`; + case OBJ_VAL_COMPLETED: + return `${json},"~~":"~~"`; + + // Array cases + case ARR: + return `${json}"~~"`; + case ARR_VAL: + return _maybeFixIncompleteArrValue(json); + case ARR_VAL_STR: + return `${json}~~"`; + case ARR_VAL_COMPLETED: + return `${json},"~~"`; + } + + return json; +} + +function _maybeFixIncompleteArrValue(json: string): string { + const pos = _findLastArrayDelimiter(json); + + if (pos > -1) { + const part = json.slice(pos + 1); + + if (ALLOWED_PRIMITIVES.includes(part.trim())) { + return `${json},"~~"`; + } + + // Everything else is replaced with `"~~"` + return `${json.slice(0, pos + 1)}"~~"`; + } + + // fallback, this shouldn't happen, to be save + return json; +} + +function _findLastArrayDelimiter(json: string): number { + for (let i = json.length - 1; i >= 0; i--) { + const char = json[i]; + + if (char === ',' || char === '[') { + return i; + } + } + + return -1; +} + +function _maybeFixIncompleteObjValue(json: string): string { + const startPos = json.lastIndexOf(':'); + + const part = json.slice(startPos + 1); + + if (ALLOWED_PRIMITIVES.includes(part.trim())) { + return `${json},"~~":"~~"`; + } + + // Everything else is replaced with `"~~"` + // This also means we do not have incomplete numbers, e.g `[1` is replaced with `["~~"]` + return `${json.slice(0, startPos + 1)}"~~"`; +} diff --git a/packages/replay/src/util/truncateJson/constants.ts b/packages/replay/src/util/truncateJson/constants.ts new file mode 100644 index 000000000000..6ea4f2dda3e2 --- /dev/null +++ b/packages/replay/src/util/truncateJson/constants.ts @@ -0,0 +1,23 @@ +export const OBJ = 10; +export const OBJ_KEY = 11; +export const OBJ_KEY_STR = 12; +export const OBJ_VAL = 13; +export const OBJ_VAL_STR = 14; +export const OBJ_VAL_COMPLETED = 15; + +export const ARR = 20; +export const ARR_VAL = 21; +export const ARR_VAL_STR = 22; +export const ARR_VAL_COMPLETED = 23; + +export type JsonToken = + | typeof OBJ + | typeof OBJ_KEY + | typeof OBJ_KEY_STR + | typeof OBJ_VAL + | typeof OBJ_VAL_STR + | typeof OBJ_VAL_COMPLETED + | typeof ARR + | typeof ARR_VAL + | typeof ARR_VAL_STR + | typeof ARR_VAL_COMPLETED; diff --git a/packages/replay/src/util/truncateJson/evaluateJson.ts b/packages/replay/src/util/truncateJson/evaluateJson.ts new file mode 100644 index 000000000000..0ba8d79c4c9a --- /dev/null +++ b/packages/replay/src/util/truncateJson/evaluateJson.ts @@ -0,0 +1,264 @@ +import type { JsonToken } from './constants'; +import { + ARR, + ARR_VAL, + ARR_VAL_COMPLETED, + ARR_VAL_STR, + OBJ, + OBJ_KEY, + OBJ_KEY_STR, + OBJ_VAL, + OBJ_VAL_COMPLETED, + OBJ_VAL_STR, +} from './constants'; + +/** + * Evaluate an (incomplete) JSON string. + */ +export function evaluateJson(json: string): JsonToken[] { + const stack: JsonToken[] = []; + + for (let pos = 0; pos < json.length; pos++) { + _evaluateJsonPos(stack, json, pos); + } + + return stack; +} + +function _evaluateJsonPos(stack: JsonToken[], json: string, pos: number): void { + const curStep = stack[stack.length - 1]; + + const char = json[pos]; + + const whitespaceRegex = /\s/; + + if (whitespaceRegex.test(char)) { + return; + } + + if (char === '"' && !_isEscaped(json, pos)) { + _handleQuote(stack, curStep); + return; + } + + switch (char) { + case '{': + _handleObj(stack, curStep); + break; + case '[': + _handleArr(stack, curStep); + break; + case ':': + _handleColon(stack, curStep); + break; + case ',': + _handleComma(stack, curStep); + break; + case '}': + _handleObjClose(stack, curStep); + break; + case ']': + _handleArrClose(stack, curStep); + break; + } +} + +function _handleQuote(stack: JsonToken[], curStep: JsonToken): void { + // End of obj value + if (curStep === OBJ_VAL_STR) { + stack.pop(); + stack.push(OBJ_VAL_COMPLETED); + return; + } + + // End of arr value + if (curStep === ARR_VAL_STR) { + stack.pop(); + stack.push(ARR_VAL_COMPLETED); + return; + } + + // Start of obj value + if (curStep === OBJ_VAL) { + stack.push(OBJ_VAL_STR); + return; + } + + // Start of arr value + if (curStep === ARR_VAL) { + stack.push(ARR_VAL_STR); + return; + } + + // Start of obj key + if (curStep === OBJ) { + stack.push(OBJ_KEY_STR); + return; + } + + // End of obj key + if (curStep === OBJ_KEY_STR) { + stack.pop(); + stack.push(OBJ_KEY); + return; + } +} + +function _handleObj(stack: JsonToken[], curStep: JsonToken): void { + // Initial object + if (!curStep) { + stack.push(OBJ); + return; + } + + // New object as obj value + if (curStep === OBJ_VAL) { + stack.push(OBJ); + return; + } + + // New object as array element + if (curStep === ARR_VAL) { + stack.push(OBJ); + } + + // New object as first array element + if (curStep === ARR) { + stack.push(OBJ); + return; + } +} + +function _handleArr(stack: JsonToken[], curStep: JsonToken): void { + // Initial array + if (!curStep) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } + + // New array as obj value + if (curStep === OBJ_VAL) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } + + // New array as array element + if (curStep === ARR_VAL) { + stack.push(ARR); + stack.push(ARR_VAL); + } + + // New array as first array element + if (curStep === ARR) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } +} + +function _handleColon(stack: JsonToken[], curStep: JsonToken): void { + if (curStep === OBJ_KEY) { + stack.pop(); + stack.push(OBJ_VAL); + } +} + +function _handleComma(stack: JsonToken[], curStep: JsonToken): void { + // Comma after obj value + if (curStep === OBJ_VAL) { + stack.pop(); + return; + } + if (curStep === OBJ_VAL_COMPLETED) { + // Pop OBJ_VAL_COMPLETED & OBJ_VAL + stack.pop(); + stack.pop(); + return; + } + + // Comma after arr value + if (curStep === ARR_VAL) { + // do nothing - basically we'd pop ARR_VAL but add it right back + return; + } + + if (curStep === ARR_VAL_COMPLETED) { + // Pop ARR_VAL_COMPLETED + stack.pop(); + + // basically we'd pop ARR_VAL but add it right back + return; + } +} + +function _handleObjClose(stack: JsonToken[], curStep: JsonToken): void { + // Empty object {} + if (curStep === OBJ) { + stack.pop(); + } + + // Object with element + if (curStep === OBJ_VAL) { + // Pop OBJ_VAL, OBJ + stack.pop(); + stack.pop(); + } + + // Obj with element + if (curStep === OBJ_VAL_COMPLETED) { + // Pop OBJ_VAL_COMPLETED, OBJ_VAL, OBJ + stack.pop(); + stack.pop(); + stack.pop(); + } + + // if was obj value, complete it + if (stack[stack.length - 1] === OBJ_VAL) { + stack.push(OBJ_VAL_COMPLETED); + } + + // if was arr value, complete it + if (stack[stack.length - 1] === ARR_VAL) { + stack.push(ARR_VAL_COMPLETED); + } +} + +function _handleArrClose(stack: JsonToken[], curStep: JsonToken): void { + // Empty array [] + if (curStep === ARR) { + stack.pop(); + } + + // Array with element + if (curStep === ARR_VAL) { + // Pop ARR_VAL, ARR + stack.pop(); + stack.pop(); + } + + // Array with element + if (curStep === ARR_VAL_COMPLETED) { + // Pop ARR_VAL_COMPLETED, ARR_VAL, ARR + stack.pop(); + stack.pop(); + stack.pop(); + } + + // if was obj value, complete it + if (stack[stack.length - 1] === OBJ_VAL) { + stack.push(OBJ_VAL_COMPLETED); + } + + // if was arr value, complete it + if (stack[stack.length - 1] === ARR_VAL) { + stack.push(ARR_VAL_COMPLETED); + } +} + +function _isEscaped(str: string, pos: number): boolean { + const previousChar = str[pos - 1]; + + return previousChar === '\\' && !_isEscaped(str, pos - 1); +} diff --git a/packages/replay/src/util/truncateJson/fixJson.ts b/packages/replay/src/util/truncateJson/fixJson.ts new file mode 100644 index 000000000000..b54d80f011c3 --- /dev/null +++ b/packages/replay/src/util/truncateJson/fixJson.ts @@ -0,0 +1,14 @@ +/* eslint-disable max-lines */ + +import { completeJson } from './completeJson'; +import { evaluateJson } from './evaluateJson'; + +/** + * Takes an incomplete JSON string, and returns a hopefully valid JSON string. + * Note that this _can_ fail, so you should check the return value is valid JSON. + */ +export function fixJson(incompleteJson: string): string { + const stack = evaluateJson(incompleteJson); + + return completeJson(incompleteJson, stack); +} diff --git a/packages/replay/test/fixtures/fixJson/1_completeJson.json b/packages/replay/test/fixtures/fixJson/1_completeJson.json new file mode 100644 index 000000000000..3865127e5a23 --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/1_completeJson.json @@ -0,0 +1 @@ +[{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-04-28T20:30:27.149789Z","lastLogin":"2023-04-13T19:40:13.734339Z","has2fa":true,"lastActive":"2023-04-14T17:36:23.756369Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"manager","roleName":"Manager","orgRole":"manager","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-04-28T20:30:27.572480Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-24T13:30:01.719798Z","lastLogin":"2023-04-18T20:19:32.572869Z","has2fa":true,"lastActive":"2023-04-18T21:18:53.674493Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-24T13:30:02.620935Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-07-26T17:38:43.681246Z","lastLogin":"2023-04-18T20:54:31.433191Z","has2fa":true,"lastActive":"2023-04-18T21:11:15.027754Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"219db453b9ef4a47a6c071fc836752a8"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-07-26T17:38:44.185908Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-05-12T17:47:51.719114Z","lastLogin":"2022-12-20T23:26:24.301088Z","has2fa":true,"lastActive":"2023-01-03T23:40:16.202147Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-05-12T17:47:52.127039Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-08-08T16:44:23.558695Z","lastLogin":"2023-04-07T07:52:12.808207Z","has2fa":true,"lastActive":"2023-04-18T17:53:21.169085Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"4548fe09d6c949ecb027c70af1d1e8fd"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-08-10T11:13:06.618597Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2023-02-01T19:03:04.268258Z","lastLogin":"2023-04-11T17:41:59.073515Z","has2fa":true,"lastActive":"2023-04-17T23:10:34.614992Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-02-03T19:52:50.750648Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-09-19T20:21:02.168949Z","lastLogin":"2023-04-17T20:27:01.536887Z","has2fa":true,"lastActive":"2023-04-18T21:09:26.940838Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"9e67f1e04e184833b8b3bfa45beea7ea"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-01-05T15:31:45.227526Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2021-08-10T13:59:05.267005Z","lastLogin":"2023-04-18T17:28:59.989484Z","has2fa":true,"lastActive":"2023-04-18T17:28:50.801466Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":"5b2c870c491841d0bc114c9df5bb6d2e"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-10T13:59:06.023687Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-05T22:12:41.902738Z","lastLogin":"2023-04-18T21:02:34.963627Z","has2fa":true,"lastActive":"2023-04-18T21:17:28.257879Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-09-16T18:05:51.667528Z","inviteStatus":"approved","inviterName":"richard.ma@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-10-14T23:15:17.528252Z","lastLogin":"2023-03-31T20:53:37.291706Z","has2fa":false,"lastActive":"2023-04-18T20:12:14.438701Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-10-19T19:38:46.936422Z","inviteStatus":"approved","inviterName":"isabel.matwawana@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-12-09T23:20:23.346432Z","lastLogin":"2022-12-09T23:20:23.765878Z","has2fa":false,"lastActive":"2022-12-23T02:01:40.052033Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-12-09T23:20:23.745693Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2018-02-26T23:57:43.766558Z","lastLogin":"2023-01-19T19:11:45.061167Z","has2fa":true,"lastActive":"2023-01-19T19:11:31.930426Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2018-02-26T16:04:56.738643Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-03-07T14:05:53.423324Z","lastLogin":"2023-04-18T07:29:18.311034Z","has2fa":true,"lastActive":"2023-04-18T16:17:56.241831Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1836fd44387a413a917cf052523623cc"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-03-07T14:05:54.238336Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-02-07T14:25:08.448480Z","lastLogin":"2023-04-13T08:24:55.740046Z","has2fa":true,"lastActive":"2023-04-18T17:53:19.914958Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"835ade1ffa314f788b1e1015af13cacb"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-02-07T14:25:08.946588Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2017-11-15T09:07:16.014572Z","lastLogin":"2023-04-18T17:50:00.282464Z","has2fa":true,"lastActive":"2023-04-18T17:55:17.126066Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2017-11-15T09:07:16.036013Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-02-08T21:51:46.074436Z","lastLogin":"2023-04-18T20:03:13.403607Z","has2fa":true,"lastActive":"2023-04-18T20:35:34.150539Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-02-08T21:51:46.795772Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-06-27T18:14:49.652095Z","lastLogin":"2023-04-18T18:26:07.379897Z","has2fa":true,"lastActive":"2023-04-18T21:22:08.385362Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1fc882a55f7e43caad0069765a940d72"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-06-27T18:14:50.014771Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-10-01T12:26:22.368368Z","lastLogin":"2023-04-18T16:28:34.978170Z","has2fa":true,"lastActive":"2023-04-18T17:53:44.185207Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-10-01T12:26:22.971086Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.p~~"}}] diff --git a/packages/replay/test/fixtures/fixJson/1_incompleteJson.txt b/packages/replay/test/fixtures/fixJson/1_incompleteJson.txt new file mode 100644 index 000000000000..4377e08e6c9d --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/1_incompleteJson.txt @@ -0,0 +1 @@ +[{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-04-28T20:30:27.149789Z","lastLogin":"2023-04-13T19:40:13.734339Z","has2fa":true,"lastActive":"2023-04-14T17:36:23.756369Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"manager","roleName":"Manager","orgRole":"manager","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-04-28T20:30:27.572480Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-24T13:30:01.719798Z","lastLogin":"2023-04-18T20:19:32.572869Z","has2fa":true,"lastActive":"2023-04-18T21:18:53.674493Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-24T13:30:02.620935Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-07-26T17:38:43.681246Z","lastLogin":"2023-04-18T20:54:31.433191Z","has2fa":true,"lastActive":"2023-04-18T21:11:15.027754Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"219db453b9ef4a47a6c071fc836752a8"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-07-26T17:38:44.185908Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-05-12T17:47:51.719114Z","lastLogin":"2022-12-20T23:26:24.301088Z","has2fa":true,"lastActive":"2023-01-03T23:40:16.202147Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-05-12T17:47:52.127039Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-08-08T16:44:23.558695Z","lastLogin":"2023-04-07T07:52:12.808207Z","has2fa":true,"lastActive":"2023-04-18T17:53:21.169085Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"4548fe09d6c949ecb027c70af1d1e8fd"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-08-10T11:13:06.618597Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2023-02-01T19:03:04.268258Z","lastLogin":"2023-04-11T17:41:59.073515Z","has2fa":true,"lastActive":"2023-04-17T23:10:34.614992Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-02-03T19:52:50.750648Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-09-19T20:21:02.168949Z","lastLogin":"2023-04-17T20:27:01.536887Z","has2fa":true,"lastActive":"2023-04-18T21:09:26.940838Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"9e67f1e04e184833b8b3bfa45beea7ea"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-01-05T15:31:45.227526Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2021-08-10T13:59:05.267005Z","lastLogin":"2023-04-18T17:28:59.989484Z","has2fa":true,"lastActive":"2023-04-18T17:28:50.801466Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":"5b2c870c491841d0bc114c9df5bb6d2e"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-10T13:59:06.023687Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-05T22:12:41.902738Z","lastLogin":"2023-04-18T21:02:34.963627Z","has2fa":true,"lastActive":"2023-04-18T21:17:28.257879Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-09-16T18:05:51.667528Z","inviteStatus":"approved","inviterName":"richard.ma@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-10-14T23:15:17.528252Z","lastLogin":"2023-03-31T20:53:37.291706Z","has2fa":false,"lastActive":"2023-04-18T20:12:14.438701Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-10-19T19:38:46.936422Z","inviteStatus":"approved","inviterName":"isabel.matwawana@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-12-09T23:20:23.346432Z","lastLogin":"2022-12-09T23:20:23.765878Z","has2fa":false,"lastActive":"2022-12-23T02:01:40.052033Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-12-09T23:20:23.745693Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2018-02-26T23:57:43.766558Z","lastLogin":"2023-01-19T19:11:45.061167Z","has2fa":true,"lastActive":"2023-01-19T19:11:31.930426Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2018-02-26T16:04:56.738643Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-03-07T14:05:53.423324Z","lastLogin":"2023-04-18T07:29:18.311034Z","has2fa":true,"lastActive":"2023-04-18T16:17:56.241831Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1836fd44387a413a917cf052523623cc"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-03-07T14:05:54.238336Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-02-07T14:25:08.448480Z","lastLogin":"2023-04-13T08:24:55.740046Z","has2fa":true,"lastActive":"2023-04-18T17:53:19.914958Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"835ade1ffa314f788b1e1015af13cacb"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-02-07T14:25:08.946588Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2017-11-15T09:07:16.014572Z","lastLogin":"2023-04-18T17:50:00.282464Z","has2fa":true,"lastActive":"2023-04-18T17:55:17.126066Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2017-11-15T09:07:16.036013Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-02-08T21:51:46.074436Z","lastLogin":"2023-04-18T20:03:13.403607Z","has2fa":true,"lastActive":"2023-04-18T20:35:34.150539Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-02-08T21:51:46.795772Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-06-27T18:14:49.652095Z","lastLogin":"2023-04-18T18:26:07.379897Z","has2fa":true,"lastActive":"2023-04-18T21:22:08.385362Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1fc882a55f7e43caad0069765a940d72"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-06-27T18:14:50.014771Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-10-01T12:26:22.368368Z","lastLogin":"2023-04-18T16:28:34.978170Z","has2fa":true,"lastActive":"2023-04-18T17:53:44.185207Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-10-01T12:26:22.971086Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.p diff --git a/packages/replay/test/fixtures/fixJson/2_completeJson.json b/packages/replay/test/fixtures/fixJson/2_completeJson.json new file mode 100644 index 000000000000..5c418e538d75 --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/2_completeJson.json @@ -0,0 +1,22 @@ +[ + { + "id": "123456", + "email": "test.user@sentry.io", + "name": "test.user@sentry.io", + "user": { + "id": "123456", + "name": "test.user@sentry.io", + "username": "ABCDEF", + "email": "test.user@sentry.io", + "isActive": true + }, + "role": "member", + "roleName": "Member", + "orgRole": "member", + "pending": false, + "expired": false, + "dateCreated": "2021-09-24T13:30:02.620935Z", + "inviteStatus": "approved", + "inviterName": null, + "orgRolesFromTeams": [], + "projects": ["feedback", "javascript", "sentry", "sentry-tests-acceptance~~"]}] diff --git a/packages/replay/test/fixtures/fixJson/2_incompleteJson.txt b/packages/replay/test/fixtures/fixJson/2_incompleteJson.txt new file mode 100644 index 000000000000..cf7d63705010 --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/2_incompleteJson.txt @@ -0,0 +1,22 @@ +[ + { + "id": "123456", + "email": "test.user@sentry.io", + "name": "test.user@sentry.io", + "user": { + "id": "123456", + "name": "test.user@sentry.io", + "username": "ABCDEF", + "email": "test.user@sentry.io", + "isActive": true + }, + "role": "member", + "roleName": "Member", + "orgRole": "member", + "pending": false, + "expired": false, + "dateCreated": "2021-09-24T13:30:02.620935Z", + "inviteStatus": "approved", + "inviterName": null, + "orgRolesFromTeams": [], + "projects": ["feedback", "javascript", "sentry", "sentry-tests-acceptance diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index 809d3717f023..b47a849f868e 100644 --- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -587,7 +587,7 @@ other-header: test`; ]); }); - it('skips fetch request/response body if configured & too large', async () => { + it('truncates fetch text request/response body if configured & too large', async () => { options.captureBodies = true; const breadcrumb: Breadcrumb = { @@ -634,15 +634,96 @@ other-header: test`; request: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['TEXT_TRUNCATED'], }, }, response: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['TEXT_TRUNCATED'], + }, + }, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.fetch', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('truncates fetch JSON request/response body if configured & too large', async () => { + options.captureBodies = true; + + const largeBody = JSON.stringify({ a: LARGE_BODY }); + + const breadcrumb: Breadcrumb = { + category: 'fetch', + data: { + method: 'GET', + url: 'https://example.com', + status_code: 200, + }, + }; + + const mockResponse = { + headers: { + get: () => '', + }, + clone: () => mockResponse, + text: () => Promise.resolve(largeBody), + } as unknown as Response; + + const hint: FetchBreadcrumbHint = { + input: ['GET', { body: largeBody }], + response: mockResponse, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'fetch', + data: { + method: 'GET', + request_body_size: largeBody.length, + status_code: 200, + url: 'https://example.com', + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + statusCode: 200, + request: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + _meta: { + warnings: ['JSON_TRUNCATED'], + }, + }, + response: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + _meta: { + warnings: ['JSON_TRUNCATED'], }, }, }, @@ -854,7 +935,7 @@ other-header: test`; ]); }); - it('skip xhr request/response body if configured & body too large', async () => { + it('truncates text xhr request/response body if configured & body too large', async () => { options.captureBodies = true; const breadcrumb: Breadcrumb = { @@ -906,15 +987,95 @@ other-header: test`; request: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['TEXT_TRUNCATED'], }, }, response: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, + _meta: { + warnings: ['TEXT_TRUNCATED'], + }, + }, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.xhr', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('truncates JSON xhr request/response body if configured & body too large', async () => { + options.captureBodies = true; + + const largeBody = JSON.stringify({ a: LARGE_BODY }); + + const breadcrumb: Breadcrumb = { + category: 'xhr', + data: { + method: 'GET', + url: 'https://example.com', + status_code: 200, + }, + }; + const xhr = new XMLHttpRequest(); + Object.defineProperty(xhr, 'response', { + value: largeBody, + }); + Object.defineProperty(xhr, 'responseText', { + value: largeBody, + }); + const hint: XhrBreadcrumbHint = { + xhr, + input: largeBody, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'xhr', + data: { + method: 'GET', + request_body_size: largeBody.length, + response_body_size: largeBody.length, + status_code: 200, + url: 'https://example.com', + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + statusCode: 200, + request: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + _meta: { + warnings: ['JSON_TRUNCATED'], + }, + }, + response: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['JSON_TRUNCATED'], }, }, }, diff --git a/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts b/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts index 2d407221b77a..f187fbe59de0 100644 --- a/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts +++ b/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts @@ -1,6 +1,10 @@ import { TextEncoder } from 'util'; -import { getBodySize, parseContentLengthHeader } from '../../../../src/coreHandlers/util/networkUtils'; +import { + buildNetworkRequestOrResponse, + getBodySize, + parseContentLengthHeader, +} from '../../../../src/coreHandlers/util/networkUtils'; jest.useFakeTimers(); @@ -62,4 +66,125 @@ describe('Unit | coreHandlers | util | networkUtils', () => { expect(getBodySize(arrayBuffer, textEncoder)).toBe(8); }); }); + + describe('buildNetworkRequestOrResponse', () => { + it.each([ + ['just text', 'just text', undefined], + ['[invalid JSON]', '[invalid JSON]', { warnings: ['INVALID_JSON'] }], + ['{invalid JSON}', '{invalid JSON}', { warnings: ['INVALID_JSON'] }], + ['[]', [], undefined], + [JSON.stringify([1, 'a', true, null, undefined]), [1, 'a', true, null, null], undefined], + [JSON.stringify([1, [2, [3, [4, [5, [6, [7, [8]]]]]]]]), [1, [2, [3, [4, [5, [6, [7, [8]]]]]]]], undefined], + ['{}', {}, undefined], + [ + JSON.stringify({ a: 1, b: true, c: 'yes', d: null, e: undefined }), + { a: 1, b: true, c: 'yes', d: null, e: undefined }, + undefined, + ], + [ + JSON.stringify({ + a: 1, + b: { + c: 2, + d: { + e: 3, + f: { + g: 4, + h: { + i: 5, + j: { + k: 6, + l: { + m: 7, + n: { + o: 8, + }, + }, + }, + }, + }, + }, + }, + }), + { + a: 1, + b: { + c: 2, + d: { + e: 3, + f: { + g: 4, + h: { + i: 5, + j: { + k: 6, + l: { + m: 7, + n: { + o: 8, + }, + }, + }, + }, + }, + }, + }, + }, + undefined, + ], + [ + JSON.stringify({ + data: { + user: { + name: 'John', + age: 42, + friends: [ + { + name: 'Jane', + }, + { + name: 'Bob', + children: [ + { name: 'Alice' }, + { + name: 'Rose', + hobbies: [{ name: 'Dancing' }, { name: 'Programming' }, { name: 'Dueling' }], + }, + ], + }, + ], + }, + }, + }), + { + data: { + user: { + name: 'John', + age: 42, + friends: [ + { + name: 'Jane', + }, + { + name: 'Bob', + children: [ + { name: 'Alice' }, + { + name: 'Rose', + hobbies: [{ name: 'Dancing' }, { name: 'Programming' }, { name: 'Dueling' }], + }, + ], + }, + ], + }, + }, + }, + undefined, + ], + ])('works with %s', (input, expectedBody, expectedMeta) => { + const actual = buildNetworkRequestOrResponse({}, 1, input); + + expect(actual).toEqual({ size: 1, headers: {}, body: expectedBody, _meta: expectedMeta }); + }); + }); }); diff --git a/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts b/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts new file mode 100644 index 000000000000..d7c294b42262 --- /dev/null +++ b/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts @@ -0,0 +1,82 @@ +import fs from 'fs'; +import path from 'path'; + +import { fixJson } from '../../../../../src/util/truncateJson/fixJson'; + +describe('Unit | coreHandlers | util | truncateJson | fixJson', () => { + test.each([ + // Basic steps of object completion + ['{', '{"~~":"~~"}'], + ['{}', '{}'], + ['{"', '{"~~":"~~"}'], + ['{"a', '{"a~~":"~~"}'], + ['{"aa', '{"aa~~":"~~"}'], + ['{"aa"', '{"aa":"~~"}'], + ['{"aa":', '{"aa":"~~"}'], + ['{"aa":"', '{"aa":"~~"}'], + ['{"aa":"b', '{"aa":"b~~"}'], + ['{"aa":"bb', '{"aa":"bb~~"}'], + ['{"aa":"bb"', '{"aa":"bb","~~":"~~"}'], + ['{"aa":"bb"}', '{"aa":"bb"}'], + + // Basic steps of array completion + ['[', '["~~"]'], + ['[]', '[]'], + ['["', '["~~"]'], + ['["a', '["a~~"]'], + ['["aa', '["aa~~"]'], + ['["aa"', '["aa","~~"]'], + ['["aa",', '["aa","~~"]'], + ['["aa","', '["aa","~~"]'], + ['["aa","b', '["aa","b~~"]'], + ['["aa","bb', '["aa","bb~~"]'], + ['["aa","bb"', '["aa","bb","~~"]'], + ['["aa","bb"]', '["aa","bb"]'], + + // Nested object/arrays + ['{"a":{"bb', '{"a":{"bb~~":"~~"}}'], + ['{"a":["bb",["cc","d', '{"a":["bb",["cc","d~~"]]}'], + + // Handles special characters in strings + ['{"a":"hel\\"lo', '{"a":"hel\\"lo~~"}'], + ['{"a":["this is }{some][ thing', '{"a":["this is }{some][ thing~~"]}'], + ['{"a:a', '{"a:a~~":"~~"}'], + ['{"a:', '{"a:~~":"~~"}'], + + // Handles incomplete non-string values + ['{"a":true', '{"a":true,"~~":"~~"}'], + ['{"a":false', '{"a":false,"~~":"~~"}'], + ['{"a":null', '{"a":null,"~~":"~~"}'], + ['{"a":tr', '{"a":"~~"}'], + ['{"a":1', '{"a":"~~"}'], + ['{"a":12', '{"a":"~~"}'], + ['[12', '["~~"]'], + ['[true', '[true,"~~"]'], + ['{"a":1', '{"a":"~~"}'], + ['{"a":tr', '{"a":"~~"}'], + ['{"a":true', '{"a":true,"~~":"~~"}'], + + // Handles whitespace + ['{"a" : true', '{"a" : true,"~~":"~~"}'], + ['{"a" : "aa', '{"a" : "aa~~"}'], + ['[1, 2, "a ", ', '[1, 2, "a ","~~"]'], + ['[1, 2, true ', '[1, 2, true ,"~~"]'], + // Complex nested JSON + ['{"aa":{"bb":"yes","cc":true},"xx":["aa",1,true', '{"aa":{"bb":"yes","cc":true},"xx":["aa",1,true,"~~"]}'], + ])('it works for %s', (json, expected) => { + const actual = fixJson(json); + expect(actual).toEqual(expected); + }); + + test.each(['1', '2'])('it works for fixture %s_incompleteJson.txt', fixture => { + const input = fs + .readFileSync(path.resolve(__dirname, `../../../../fixtures/fixJson/${fixture}_incompleteJson.txt`), 'utf8') + .trim(); + const expected = fs + .readFileSync(path.resolve(__dirname, `../../../../fixtures/fixJson/${fixture}_completeJson.json`), 'utf8') + .trim(); + + const actual = fixJson(input); + expect(actual).toEqual(expected); + }); +}); From 6e5cb41951f9a86248a649f0c928975fec416443 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Apr 2023 12:59:08 +0200 Subject: [PATCH 26/28] fix(replay): Ensure we normalize scope breadcrumbs to max. depth to avoid circular ref (#7915) --- packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts b/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts index 26d950d9a261..cbb998d499d4 100644 --- a/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts +++ b/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts @@ -27,7 +27,8 @@ export function addBreadcrumbEvent(replay: ReplayContainer, breadcrumb: Breadcru timestamp: (breadcrumb.timestamp || 0) * 1000, data: { tag: 'breadcrumb', - payload: normalize(breadcrumb), + // normalize to max. 10 depth and 1_000 properties per object + payload: normalize(breadcrumb, 10, 1_000), }, }); From cd7a51334ae4776a506b2747046ad1381366260d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Apr 2023 16:08:08 +0200 Subject: [PATCH 27/28] fix(utils): Normalize HTML elements as string (#7916) Currently, we do not special-case html elements in normalization. This means they are still normalized as objects, leading to potentially deeply nested stuff, and to problems with e.g. replay. --- .../non_serializable_context/test.ts | 2 +- packages/utils/src/normalize.ts | 11 +++++++- packages/utils/test/normalize.test.ts | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/browser-integration-tests/suites/public-api/setContext/non_serializable_context/test.ts b/packages/browser-integration-tests/suites/public-api/setContext/non_serializable_context/test.ts index 3c6d17dbdb03..9b270205f109 100644 --- a/packages/browser-integration-tests/suites/public-api/setContext/non_serializable_context/test.ts +++ b/packages/browser-integration-tests/suites/public-api/setContext/non_serializable_context/test.ts @@ -9,6 +9,6 @@ sentryTest('should normalize non-serializable context', async ({ getLocalTestPat const eventData = await getFirstSentryEnvelopeRequest(page, url); - expect(eventData.contexts?.non_serializable).toMatchObject({}); + expect(eventData.contexts?.non_serializable).toEqual('[HTMLElement: HTMLBodyElement]'); expect(eventData.message).toBe('non_serializable'); }); diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 508442c2d14e..4b2dd611f8e2 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -170,6 +170,7 @@ function visit( // TODO remove this in v7 (this means the method will no longer be exported, under any name) export { visit as walk }; +/* eslint-disable complexity */ /** * Stringify the given value. Handles various known special values and types. * @@ -242,11 +243,19 @@ function stringifyValue( // them to strings means that instances of classes which haven't defined their `toStringTag` will just come out as // `"[object Object]"`. If we instead look at the constructor's name (which is the same as the name of the class), // we can make sure that only plain objects come out that way. - return `[object ${getConstructorName(value)}]`; + const objName = getConstructorName(value); + + // Handle HTML Elements + if (/^HTML(\w*)Element$/.test(objName)) { + return `[HTMLElement: ${objName}]`; + } + + return `[object ${objName}]`; } catch (err) { return `**non-serializable** (${err})`; } } +/* eslint-enable complexity */ function getConstructorName(value: unknown): string { const prototype: Prototype | null = Object.getPrototypeOf(value); diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index 94676c1449da..008bde5dfebe 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -263,6 +263,32 @@ describe('normalize()', () => { }); }); + describe('handles HTML elements', () => { + test('HTMLDivElement', () => { + expect( + normalize({ + div: document.createElement('div'), + div2: document.createElement('div'), + }), + ).toEqual({ + div: '[HTMLElement: HTMLDivElement]', + div2: '[HTMLElement: HTMLDivElement]', + }); + }); + + test('input elements', () => { + expect( + normalize({ + input: document.createElement('input'), + select: document.createElement('select'), + }), + ).toEqual({ + input: '[HTMLElement: HTMLInputElement]', + select: '[HTMLElement: HTMLSelectElement]', + }); + }); + }); + describe('calls toJSON if implemented', () => { test('primitive values', () => { const a = new Number(1) as any; From 0b65590d8b1f9018e766efd6d4bd684ee55cf443 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Apr 2023 16:09:26 +0200 Subject: [PATCH 28/28] meta(changelog): Update CHANGELOG for 7.49.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd32edeee293..2241d8cb8442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.49.0 + +### Important Changes + +- **feat(sveltekit): Read adapter output directory from `svelte.config.js` (#7863)** + +Our source maps upload plugin is now able to read `svelte.config.js`. This is necessary to automatically find the output directory that users can specify when setting up the Node adapter. + +- **fix(replay): Ensure we normalize scope breadcrumbs to max. depth to avoid circular ref (#7915)** + +This release fixes a potential problem with how Replay captures console logs. +Any objects logged will now be cut off after a maximum depth of 10, as well as cutting off any properties after the 1000th. +This should ensure we do not accidentally capture massive console logs, where a stringified object could reach 100MB or more. + +- **fix(utils): Normalize HTML elements as string (#7916)** + +We used to normalize references to HTML elements as POJOs. +This is both not very easily understandable, as well as potentially large, as HTML elements may have properties attached to them. +With this change, we now normalize them to e.g. `[HTMLElement: HTMLInputElement]`. + +### Additional Features and Fixes + +- feat(browser): Simplify stack parsers (#7897) +- feat(node): Add monitor upsert types (#7914) +- feat(replay): Truncate network bodies to max size (#7875) +- fix(gatsby): Don't crash build when auth token is missing (#7858) +- fix(gatsby): Use `import` for `gatsby-browser.js` instead of `require` (#7889) +- fix(nextjs): Handle braces in stack frame URLs (#7900) +- fix(nextjs): Mark value injection loader result as uncacheable (#7870) +- fix(node): Correct typo in trpc integration transaciton name (#7871) +- fix(node): reduce deepReadDirSync runtime complexity (#7910) +- fix(sveltekit): Avoid capturing "Not Found" errors in server `handleError` wrapper (#7898) +- fix(sveltekit): Detect sentry release before creating the Vite plugins (#7902) +- fix(sveltekit): Use `sentry.properties` file when uploading source maps (#7890) +- fix(tracing): Ensure we use s instead of ms for startTimestamp (#7877) +- ref(deprecate): Deprecate `timestampWithMs` (#7878) +- ref(nextjs): Don't use Sentry Webpack Plugin in dev mode (#7901) + ## 7.48.0 ### Important Changes