Skip to content

meta(changelog): Update changelog for 7.63.0 #8788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

## 7.63.0

- build(deps): bump @opentelemetry/instrumentation from 0.41.0 to 0.41.2
- feat(eventbuilder): Export `exceptionFromError` for use in hybrid SDKs (#8766)
- feat(node-experimental): Re-export from node (#8786)
- feat(tracing): Add db connection attributes for mysql spans (#8775)
- feat(tracing): Add db connection attributes for postgres spans (#8778)
- feat(tracing): Improve data collection for mongodb spans (#8774)
- fix(nextjs): Execute sentry config independently of `autoInstrumentServerFunctions` and `autoInstrumentAppDirectory` (#8781)
- fix(replay): Ensure we do not flush if flush took too long (#8784)
- fix(replay): Ensure we do not try to flush when we force stop replay (#8783)
- fix(replay): Fix `hasCheckout` handling (#8782)
- fix(replay): Handle multiple clicks in a short time (#8773)
- ref(replay): Skip events being added too long after initial segment (#8768)

## 7.62.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button onclick="console.log('Test log')" id="button1">Click me</button>
<button onclick="console.log('Test log 2')" id="button2">Click me</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser } from '../../../utils/helpers';
import {
getDecompressedRecordingEvents,
getReplaySnapshot,
isReplayEvent,
REPLAY_DEFAULT_FLUSH_MAX_DELAY,
shouldSkipReplayTest,
waitForReplayRequest,
} from '../../../utils/replayHelpers';

sentryTest(
'should stop recording when running into eventBuffer error',
async ({ getLocalTestPath, page, forceFlushReplay }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
});
});

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);

await waitForReplayRequest(page);
const replay = await getReplaySnapshot(page);
expect(replay._isEnabled).toBe(true);

await forceFlushReplay();

let called = 0;

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
const event = envelopeRequestParser(route.request());

// We only want to count replays here
if (event && isReplayEvent(event)) {
const events = getDecompressedRecordingEvents(route.request());
// this makes sure we ignore e.g. mouse move events which can otherwise lead to flakes
if (events.length > 0) {
called++;
}
}

return route.fulfill({
status: 200,
});
});

called = 0;

/**
* We test the following here:
* 1. First click should add an event (so the eventbuffer is not empty)
* 2. Second click should throw an error in eventBuffer (which should lead to stopping the replay)
* 3. Nothing should be sent to API, as we stop the replay due to the eventBuffer error.
*/
await page.evaluate(`
window._count = 0;
window._addEvent = window.Replay._replay.eventBuffer.addEvent.bind(window.Replay._replay.eventBuffer);
window.Replay._replay.eventBuffer.addEvent = (...args) => {
window._count++;
if (window._count === 2) {
throw new Error('provoked error');
}
window._addEvent(...args);
};
`);

void page.click('#button1');
void page.click('#button2');

// Should immediately skip retrying and just cancel, no backoff
// This waitForTimeout call should be okay, as we're not checking for any
// further network requests afterwards.
await page.waitForTimeout(REPLAY_DEFAULT_FLUSH_MAX_DELAY + 100);

expect(called).toBe(0);

const replay2 = await getReplaySnapshot(page);

expect(replay2._isEnabled).toBe(false);
},
);
4 changes: 2 additions & 2 deletions packages/browser-integration-tests/utils/replayHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ function getOptionsEvents(replayRequest: Request): CustomRecordingEvent[] {
return getAllCustomRrwebRecordingEvents(events).filter(data => data.tag === 'options');
}

function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] {
export function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] {
const replayRequest = getRequest(resOrReq);
return (
(replayEnvelopeRequestParser(replayRequest, 5) as eventWithTime[])
Expand Down Expand Up @@ -302,7 +302,7 @@ const replayEnvelopeRequestParser = (request: Request | null, envelopeIndex = 2)
return envelope[envelopeIndex] as Event;
};

const replayEnvelopeParser = (request: Request | null): unknown[] => {
export const replayEnvelopeParser = (request: Request | null): unknown[] => {
// https://develop.sentry.dev/sdk/envelopes/
const envelopeBytes = request?.postDataBuffer() || '';

Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export {
opera11StackLineParser,
winjsStackLineParser,
} from './stack-parsers';
export { eventFromException, eventFromMessage } from './eventbuilder';
export { eventFromException, eventFromMessage, exceptionFromError } from './eventbuilder';
export { createUserFeedbackEnvelope } from './userfeedback';
export { defaultIntegrations, forceLoad, init, onLoad, showReportDialog, wrap, captureUserFeedback } from './sdk';
export { GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, HttpContext, Dedupe } from './integrations';
6 changes: 4 additions & 2 deletions packages/nextjs/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ export default [
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: [
'src/config/templates/pageWrapperTemplate.ts',
'src/config/templates/apiWrapperTemplate.ts',
'src/config/templates/middlewareWrapperTemplate.ts',
'src/config/templates/serverComponentWrapperTemplate.ts',
'src/config/templates/pageWrapperTemplate.ts',
'src/config/templates/requestAsyncStorageShim.ts',
'src/config/templates/sentryInitWrapperTemplate.ts',
'src/config/templates/serverComponentWrapperTemplate.ts',
],

packageSpecificConfig: {
Expand All @@ -47,6 +48,7 @@ export default [
external: [
'@sentry/nextjs',
'next/dist/client/components/request-async-storage',
'__SENTRY_CONFIG_IMPORT_PATH__',
'__SENTRY_WRAPPING_TARGET_FILE__',
'__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__',
],
Expand Down
32 changes: 21 additions & 11 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const requestAsyncStorageShimPath = path.resolve(__dirname, '..', 'templates', '
const requestAsyncStorageModuleExists = moduleExists(NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH);
let showedMissingAsyncStorageModuleWarning = false;

const sentryInitWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'sentryInitWrapperTemplate.js');
const sentryInitWrapperTemplateCode = fs.readFileSync(sentryInitWrapperTemplatePath, { encoding: 'utf8' });

const serverComponentWrapperTemplatePath = path.resolve(
__dirname,
'..',
Expand All @@ -43,7 +46,7 @@ type LoaderOptions = {
appDir: string;
pageExtensionRegex: string;
excludeServerRoutes: Array<RegExp | string>;
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component';
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component' | 'sentry-init';
sentryConfigFilePath?: string;
vercelCronsConfig?: VercelCronsConfig;
};
Expand Down Expand Up @@ -83,7 +86,23 @@ export default function wrappingLoader(

let templateCode: string;

if (wrappingTargetKind === 'page' || wrappingTargetKind === 'api-route') {
if (wrappingTargetKind === 'sentry-init') {
templateCode = sentryInitWrapperTemplateCode;

// Absolute paths to the sentry config do not work with Windows: https://github.com/getsentry/sentry-javascript/issues/8133
// Se we need check whether `this.resourcePath` is absolute because there is no contract by webpack that says it is absolute.
// Examples where `this.resourcePath` could possibly be non-absolute are virtual modules.
if (sentryConfigFilePath && path.isAbsolute(this.resourcePath)) {
const sentryConfigImportPath = path
.relative(path.dirname(this.resourcePath), sentryConfigFilePath)
.replace(/\\/g, '/');
templateCode = templateCode.replace(/__SENTRY_CONFIG_IMPORT_PATH__/g, sentryConfigImportPath);
} else {
// Bail without doing any wrapping
this.callback(null, userCode, userModuleSourceMap);
return;
}
} else if (wrappingTargetKind === 'page' || wrappingTargetKind === 'api-route') {
// Get the parameterized route name from this page's filepath
const parameterizedPagesRoute = path.posix
.normalize(
Expand Down Expand Up @@ -207,15 +226,6 @@ export default function wrappingLoader(
throw new Error(`Invariant: Could not get template code of unknown kind "${wrappingTargetKind}"`);
}

// We check whether `this.resourcePath` is absolute because there is no contract by webpack that says it is absolute,
// however we can only create relative paths to the sentry config from absolute paths.Examples where this could possibly be non - absolute are virtual modules.
if (sentryConfigFilePath && path.isAbsolute(this.resourcePath)) {
const sentryConfigImportPath = path
.relative(path.dirname(this.resourcePath), sentryConfigFilePath) // Absolute paths do not work with Windows: https://github.com/getsentry/sentry-javascript/issues/8133
.replace(/\\/g, '/');
templateCode = `import "${sentryConfigImportPath}";\n`.concat(templateCode);
}

// Replace the import path of the wrapping target in the template with a path that the `wrapUserCode` function will understand.
templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET_FILE__/g, WRAPPING_TARGET_MODULE_NAME);

Expand Down
11 changes: 11 additions & 0 deletions packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-ignore This will be replaced with the user's sentry config gile
// eslint-disable-next-line import/no-unresolved
import '__SENTRY_CONFIG_IMPORT_PATH__';

// @ts-ignore This is the file we're wrapping
// eslint-disable-next-line import/no-unresolved
export * from '__SENTRY_WRAPPING_TARGET_FILE__';

// @ts-ignore This is the file we're wrapping
// eslint-disable-next-line import/no-unresolved
export { default } from '__SENTRY_WRAPPING_TARGET_FILE__';
85 changes: 57 additions & 28 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,45 @@ export function constructWebpackConfigFunction(
return path.normalize(absoluteResourcePath);
};

const isPageResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
normalizedAbsoluteResourcePath.startsWith(pagesDirPath + path.sep) &&
!normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
);
};

const isApiRouteResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
);
};

const isMiddlewareResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return normalizedAbsoluteResourcePath === middlewareJsPath || normalizedAbsoluteResourcePath === middlewareTsPath;
};

const isServerComponentResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);

// ".js, .jsx, or .tsx file extensions can be used for Pages"
// https://beta.nextjs.org/docs/routing/pages-and-layouts#pages:~:text=.js%2C%20.jsx%2C%20or%20.tsx%20file%20extensions%20can%20be%20used%20for%20Pages.
return (
normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) &&
!!normalizedAbsoluteResourcePath.match(/[\\/](page|layout|loading|head|not-found)\.(js|jsx|tsx)$/)
);
};

if (isServer && userSentryOptions.autoInstrumentServerFunctions !== false) {
// It is very important that we insert our loaders at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened.

// Wrap pages
newConfig.module.rules.unshift({
test: resourcePath => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
normalizedAbsoluteResourcePath.startsWith(pagesDirPath + path.sep) &&
!normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
);
},
test: isPageResource,
use: [
{
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
Expand Down Expand Up @@ -190,13 +216,7 @@ export function constructWebpackConfigFunction(

// Wrap api routes
newConfig.module.rules.unshift({
test: resourcePath => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
);
},
test: isApiRouteResource,
use: [
{
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
Expand All @@ -211,12 +231,7 @@ export function constructWebpackConfigFunction(

// Wrap middleware
newConfig.module.rules.unshift({
test: resourcePath => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
normalizedAbsoluteResourcePath === middlewareJsPath || normalizedAbsoluteResourcePath === middlewareTsPath
);
},
test: isMiddlewareResource,
use: [
{
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
Expand All @@ -232,22 +247,36 @@ export function constructWebpackConfigFunction(
if (isServer && userSentryOptions.autoInstrumentAppDirectory !== false) {
// Wrap page server components
newConfig.module.rules.unshift({
test: resourcePath => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
test: isServerComponentResource,
use: [
{
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
options: {
...staticWrappingLoaderOptions,
wrappingTargetKind: 'server-component',
},
},
],
});
}

// ".js, .jsx, or .tsx file extensions can be used for Pages"
// https://beta.nextjs.org/docs/routing/pages-and-layouts#pages:~:text=.js%2C%20.jsx%2C%20or%20.tsx%20file%20extensions%20can%20be%20used%20for%20Pages.
if (isServer) {
// Import the Sentry config in every user file
newConfig.module.rules.unshift({
test: resourcePath => {
return (
normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) &&
!!normalizedAbsoluteResourcePath.match(/[\\/](page|layout|loading|head|not-found)\.(js|jsx|tsx)$/)
isPageResource(resourcePath) ||
isApiRouteResource(resourcePath) ||
isMiddlewareResource(resourcePath) ||
isServerComponentResource(resourcePath)
);
},
use: [
{
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
options: {
...staticWrappingLoaderOptions,
wrappingTargetKind: 'server-component',
wrappingTargetKind: 'sentry-init',
},
},
],
Expand Down
Loading