Skip to content

Commit

Permalink
Merge pull request #8788 from getsentry/prepare-release/7.63.0
Browse files Browse the repository at this point in the history
meta(changelog): Update changelog for 7.63.0
  • Loading branch information
mydea committed Aug 10, 2023
2 parents a9fe05c + 3439d39 commit 30da5c3
Show file tree
Hide file tree
Showing 33 changed files with 850 additions and 123 deletions.
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

0 comments on commit 30da5c3

Please sign in to comment.