Skip to content

Commit

Permalink
Merge pull request #8318 from getsentry/prepare-release/7.55.0
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Jun 13, 2023
2 parents 937cbd0 + f81981d commit 0f3f211
Show file tree
Hide file tree
Showing 44 changed files with 979 additions and 406 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

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

## 7.55.0

- feat(replay): Capture slow clicks (GA) (#8298)
- feat(replay): Improve types for replay recording events (#8224)
- fix(nextjs): Strip query params from transaction names of navigations to unknown routes (#8278)
- fix(replay): Ignore max session life for buffered sessions (#8258)
- fix(sveltekit): Export captureCheckIn (#8313)
- ref(svelte): Add Svelte 4 as a peer dependency (#8280)

## 7.54.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 500,
flushMaxDelay: 500,
slowClickTimeout: 0,
});

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 0.0,

integrations: [window.Replay],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';

sentryTest('does not capture slow click when slowClickTimeout === 0', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const url = await getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page, (event, res) => {
const { breadcrumbs } = getCustomRecordingEvents(res);

return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
});

await page.click('#mutationButton');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
id: 'mutationButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ********',
},
nodeId: expect.any(Number),
},
message: 'body > button#mutationButton',
timestamp: expect.any(Number),
type: 'default',
},
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,54 @@ sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page
},
]);
});

sentryTest('click is ignored on div', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const url = await getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page, (event, res) => {
const { breadcrumbs } = getCustomRecordingEvents(res);

return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
});

await page.click('#mutationDiv');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
id: 'mutationDiv',
},
id: expect.any(Number),
tagName: 'div',
textContent: '******* ********',
},
nodeId: expect.any(Number),
},
message: 'body > div#mutationDiv',
timestamp: expect.any(Number),
type: 'default',
},
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,8 @@ window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 500,
flushMaxDelay: 500,
_experiments: {
slowClicks: {
threshold: 300,
scrollThreshold: 300,
timeout: 2000,
ignoreSelectors: ['.ignore-class', '[ignore-attribute]'],
},
},
slowClickTimeout: 3100,
slowClickIgnoreSelectors: ['.ignore-class', '[ignore-attribute]'],
});

Sentry.init({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ sentryTest('mutation after threshold results in slow click', async ({ getLocalTe
},
]);

expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeGreaterThan(300);
expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeLessThan(2000);
expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeGreaterThan(3000);
expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeLessThan(3100);
});

sentryTest('immediate mutation does not trigger slow click', async ({ browserName, getLocalTestUrl, page }) => {
Expand Down Expand Up @@ -165,56 +165,3 @@ sentryTest('inline click handler does not trigger slow click', async ({ getLocal
},
]);
});

sentryTest('click is not ignored on div', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const url = await getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page, (event, res) => {
const { breadcrumbs } = getCustomRecordingEvents(res);

return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
});

await page.click('#mutationDiv');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs.filter(({ category }) => category === 'ui.slowClickDetected')).toEqual([
{
category: 'ui.slowClickDetected',
data: {
endReason: 'mutation',
node: {
attributes: {
id: 'mutationDiv',
},
id: expect.any(Number),
tagName: 'div',
textContent: '******* ********',
},
nodeId: expect.any(Number),
timeAfterClickMs: expect.any(Number),
url: 'http://sentry-test.io/index.html',
},
message: 'body > div#mutationDiv',
timestamp: expect.any(Number),
},
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,22 @@ <h1 id="h2">Bottom</h1>
document.getElementById('mutationButton').addEventListener('click', () => {
setTimeout(() => {
document.getElementById('out').innerHTML += 'mutationButton clicked<br>';
}, 400);
}, 3001);
});
document.getElementById('mutationIgnoreButton').addEventListener('click', () => {
setTimeout(() => {
document.getElementById('out').innerHTML += 'mutationIgnoreButton clicked<br>';
}, 400);
}, 3001);
});
document.getElementById('mutationDiv').addEventListener('click', () => {
setTimeout(() => {
document.getElementById('out').innerHTML += 'mutationDiv clicked<br>';
}, 400);
}, 3001);
});
document.getElementById('mutationButtonLate').addEventListener('click', () => {
setTimeout(() => {
document.getElementById('out').innerHTML += 'mutationButtonLate clicked<br>';
}, 3000);
}, 3101);
});
document.getElementById('mutationButtonImmediately').addEventListener('click', () => {
document.getElementById('out').innerHTML += 'mutationButtonImmediately clicked<br>';
Expand All @@ -62,12 +62,12 @@ <h1 id="h2">Bottom</h1>
document.getElementById('scrollLateButton').addEventListener('click', () => {
setTimeout(() => {
document.getElementById('h2').scrollIntoView({ behavior: 'smooth' });
}, 400);
}, 3001);
});
document.getElementById('consoleLogButton').addEventListener('click', () => {
setTimeout(() => {
console.log('DONE');
}, 400);
}, 3001);
});

// Do nothing on these elements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ sentryTest('mutation after timeout results in slow click', async ({ getLocalTest
textContent: '******* ******** ****',
},
nodeId: expect.any(Number),
timeAfterClickMs: 2000,
timeAfterClickMs: 3100,
url: 'http://sentry-test.io/index.html',
},
message: 'body > button#mutationButtonLate',
Expand Down Expand Up @@ -104,7 +104,7 @@ sentryTest('console.log results in slow click', async ({ getLocalTestUrl, page }
textContent: '******* ******* ***',
},
nodeId: expect.any(Number),
timeAfterClickMs: 2000,
timeAfterClickMs: 3100,
url: 'http://sentry-test.io/index.html',
},
message: 'body > button#consoleLogButton',
Expand Down
6 changes: 5 additions & 1 deletion packages/e2e-tests/lib/runAllTestApps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export async function runAllTestApps(
recipePaths: string[],
envVarsToInject: Record<string, string | undefined>,
): Promise<void> {
const maxParallel = process.env.CI ? 3 : 6;
const maxParallel = process.env.CANARY_E2E_TEST
? 1 // TODO: figure out why concurrent tests fail for Next.js and remove this concurrency limitation
: process.env.CI
? 3
: 6;

const recipeInstances = constructRecipeInstances(recipePaths);

Expand Down
5 changes: 3 additions & 2 deletions packages/nextjs/src/client/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ export function nextRouterInstrumentation(

if (startTransactionOnLocationChange) {
Router.events.on('routeChangeStart', (navigationTarget: string) => {
const matchedRoute = getNextRouteFromPathname(stripUrlQueryAndFragment(navigationTarget));
const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget);
const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget);

let transactionName: string;
let transactionSource: TransactionSource;
Expand All @@ -152,7 +153,7 @@ export function nextRouterInstrumentation(
transactionName = matchedRoute;
transactionSource = 'route';
} else {
transactionName = navigationTarget;
transactionName = strippedNavigationTarget;
transactionSource = 'url';
}

Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/test/performance/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ describe('nextRouterInstrumentation', () => {
['/news', '/news', 'route'],
['/news/', '/news', 'route'],
['/some-route-that-is-not-defined-12332', '/some-route-that-is-not-defined-12332', 'url'], // unknown route
['/some-route-that-is-not-defined-12332?q=42', '/some-route-that-is-not-defined-12332', 'url'], // unknown route w/ query param
['/posts/42', '/posts/[id]', 'route'],
['/posts/42/', '/posts/[id]', 'route'],
['/posts/42?someParam=1', '/posts/[id]', 'route'], // query params are ignored
Expand Down
5 changes: 5 additions & 0 deletions packages/replay/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ export const NETWORK_BODY_MAX_SIZE = 150_000;

/* The max size of a single console arg that is captured. Any arg larger than this will be truncated. */
export const CONSOLE_ARG_MAX_SIZE = 5_000;

/* Min. time to wait before we consider something a slow click. */
export const SLOW_CLICK_THRESHOLD = 3_000;
/* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */
export const SLOW_CLICK_SCROLL_TIMEOUT = 300;
13 changes: 7 additions & 6 deletions packages/replay/src/coreHandlers/handleDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NodeType } from '@sentry-internal/rrweb-snapshot';
import type { Breadcrumb } from '@sentry/types';
import { htmlTreeAsString } from '@sentry/utils';

import { SLOW_CLICK_SCROLL_TIMEOUT, SLOW_CLICK_THRESHOLD } from '../constants';
import type { ReplayContainer, SlowClickConfig } from '../types';
import { createBreadcrumb } from '../util/createBreadcrumb';
import { detectSlowClick } from './handleSlowClick';
Expand All @@ -17,14 +18,14 @@ export interface DomHandlerData {
export const handleDomListener: (replay: ReplayContainer) => (handlerData: DomHandlerData) => void = (
replay: ReplayContainer,
) => {
const slowClickExperiment = replay.getOptions()._experiments.slowClicks;
const { slowClickTimeout, slowClickIgnoreSelectors } = replay.getOptions();

const slowClickConfig: SlowClickConfig | undefined = slowClickExperiment
const slowClickConfig: SlowClickConfig | undefined = slowClickTimeout
? {
threshold: slowClickExperiment.threshold,
timeout: slowClickExperiment.timeout,
scrollTimeout: slowClickExperiment.scrollTimeout,
ignoreSelector: slowClickExperiment.ignoreSelectors ? slowClickExperiment.ignoreSelectors.join(',') : '',
threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
timeout: slowClickTimeout,
scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
}
: undefined;

Expand Down
5 changes: 4 additions & 1 deletion packages/replay/src/coreHandlers/handleKeyboardEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function handleKeyboardEvent(replay: ReplayContainer, event: KeyboardEven
return;
}

replay.triggerUserActivity();
// Update user activity, but do not restart recording as it can create
// noisy/low-value replays (e.g. user comes back from idle, hits alt-tab, new
// session with a single "keydown" breadcrumb is created)
replay.updateUserActivity();

const breadcrumb = getKeyboardBreadcrumb(event);

Expand Down

0 comments on commit 0f3f211

Please sign in to comment.