Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(replay): Capture keyboard presses for special characters (#8051)
- Loading branch information
Showing
7 changed files
with
354 additions
and
33 deletions.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
packages/browser-integration-tests/suites/replay/keyboardEvents/init.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.Sentry = Sentry; | ||
window.Replay = new Sentry.Replay({ | ||
flushMinDelay: 1000, | ||
flushMaxDelay: 1000, | ||
}); | ||
|
||
Sentry.init({ | ||
dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
sampleRate: 0, | ||
replaysSessionSampleRate: 1.0, | ||
replaysOnErrorSampleRate: 0.0, | ||
|
||
integrations: [window.Replay], | ||
}); |
9 changes: 9 additions & 0 deletions
9
packages/browser-integration-tests/suites/replay/keyboardEvents/template.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
</head> | ||
<body> | ||
<input id="input" /> | ||
</body> | ||
</html> |
111 changes: 111 additions & 0 deletions
111
packages/browser-integration-tests/suites/replay/keyboardEvents/test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { expect } from '@playwright/test'; | ||
|
||
import { sentryTest } from '../../../utils/fixtures'; | ||
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; | ||
|
||
sentryTest('captures keyboard events', async ({ forceFlushReplay, getLocalTestPath, 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 getLocalTestPath({ testDir: __dirname }); | ||
|
||
await page.goto(url); | ||
await reqPromise0; | ||
await forceFlushReplay(); | ||
|
||
const reqPromise1 = waitForReplayRequest(page, (event, res) => { | ||
return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.keyDown'); | ||
}); | ||
const reqPromise2 = waitForReplayRequest(page, (event, res) => { | ||
return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.input'); | ||
}); | ||
|
||
// Trigger keyboard unfocused | ||
await page.keyboard.press('a'); | ||
await page.keyboard.press('Control+A'); | ||
|
||
// Type unfocused | ||
await page.keyboard.type('Hello', { delay: 10 }); | ||
|
||
// Type focused | ||
await page.locator('#input').focus(); | ||
|
||
await page.keyboard.press('Control+A'); | ||
await page.keyboard.type('Hello', { delay: 10 }); | ||
|
||
await forceFlushReplay(); | ||
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); | ||
const { breadcrumbs: breadcrumbs2 } = getCustomRecordingEvents(await reqPromise2); | ||
|
||
// Combine the two together | ||
// Usually, this should all be in a single request, but it _may_ be split out, so we combine this together here. | ||
breadcrumbs2.forEach(breadcrumb => { | ||
if (!breadcrumbs.some(b => b.category === breadcrumb.category && b.timestamp === breadcrumb.timestamp)) { | ||
breadcrumbs.push(breadcrumb); | ||
} | ||
}); | ||
|
||
expect(breadcrumbs).toEqual([ | ||
{ | ||
timestamp: expect.any(Number), | ||
type: 'default', | ||
category: 'ui.keyDown', | ||
message: 'body', | ||
data: { | ||
nodeId: expect.any(Number), | ||
node: { | ||
attributes: {}, | ||
id: expect.any(Number), | ||
tagName: 'body', | ||
textContent: '', | ||
}, | ||
metaKey: false, | ||
shiftKey: false, | ||
ctrlKey: true, | ||
altKey: false, | ||
key: 'Control', | ||
}, | ||
}, | ||
{ | ||
timestamp: expect.any(Number), | ||
type: 'default', | ||
category: 'ui.keyDown', | ||
message: 'body', | ||
data: { | ||
nodeId: expect.any(Number), | ||
node: { attributes: {}, id: expect.any(Number), tagName: 'body', textContent: '' }, | ||
metaKey: false, | ||
shiftKey: false, | ||
ctrlKey: true, | ||
altKey: false, | ||
key: 'A', | ||
}, | ||
}, | ||
{ | ||
timestamp: expect.any(Number), | ||
type: 'default', | ||
category: 'ui.input', | ||
message: 'body > input#input', | ||
data: { | ||
nodeId: expect.any(Number), | ||
node: { | ||
attributes: { id: 'input' }, | ||
id: expect.any(Number), | ||
tagName: 'input', | ||
textContent: '', | ||
}, | ||
}, | ||
}, | ||
]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import type { Breadcrumb } from '@sentry/types'; | ||
import { htmlTreeAsString } from '@sentry/utils'; | ||
|
||
import type { ReplayContainer } from '../types'; | ||
import { createBreadcrumb } from '../util/createBreadcrumb'; | ||
import { getBaseDomBreadcrumb } from './handleDom'; | ||
import { addBreadcrumbEvent } from './util/addBreadcrumbEvent'; | ||
|
||
/** Handle keyboard events & create breadcrumbs. */ | ||
export function handleKeyboardEvent(replay: ReplayContainer, event: KeyboardEvent): void { | ||
if (!replay.isEnabled()) { | ||
return; | ||
} | ||
|
||
replay.triggerUserActivity(); | ||
|
||
const breadcrumb = getKeyboardBreadcrumb(event); | ||
|
||
if (!breadcrumb) { | ||
return; | ||
} | ||
|
||
addBreadcrumbEvent(replay, breadcrumb); | ||
} | ||
|
||
/** exported only for tests */ | ||
export function getKeyboardBreadcrumb(event: KeyboardEvent): Breadcrumb | null { | ||
const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event; | ||
|
||
// never capture for input fields | ||
if (!target || isInputElement(target as HTMLElement)) { | ||
return null; | ||
} | ||
|
||
// Note: We do not consider shift here, as that means "uppercase" | ||
const hasModifierKey = metaKey || ctrlKey || altKey; | ||
const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length | ||
|
||
// Do not capture breadcrumb if only a word key is pressed | ||
// This could leak e.g. user input | ||
if (!hasModifierKey && isCharacterKey) { | ||
return null; | ||
} | ||
|
||
const message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>'; | ||
const baseBreadcrumb = getBaseDomBreadcrumb(target as Node, message); | ||
|
||
return createBreadcrumb({ | ||
category: 'ui.keyDown', | ||
message, | ||
data: { | ||
...baseBreadcrumb.data, | ||
metaKey, | ||
shiftKey, | ||
ctrlKey, | ||
altKey, | ||
key, | ||
}, | ||
}); | ||
} | ||
|
||
function isInputElement(target: HTMLElement): boolean { | ||
return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.