Lessons Learned from building Session Replay - Francesco Novy
- 2. About Me
Sentry & Session Replay
How does Session Replay work?
Lessons Learned:
Know your Use Case
Expect the Unexpected
How to Optimize Bundle Size
How to Compress Data
Overview
2
- 6. Capture what's happening in the user's
browser
See what happened leading up to an
error
In-depth debugging similar to the
Browser DevTools
Session Replay
6
- 9. import * as Sentry from '@sentry/browser';
Sentry.init({
integrations: [
new Sentry.Replay({
unmask: ['.show-this-class']
})
],
// Always capture when an error happens
replaysOnErrorSampleRate: 1.0,
// Capture 10% of sessions generally
replaysSessionSampleRate: 0.1,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
How to use Session Replay?
9
- 10. We use a fork of
Mutation Observers
Monkey Patching* Stylesheet APIs
Monkey Patching* Canvas APIs
rrweb
How does it work?
10
- 11. * What is Monkey Patching?
const originalFetch = window.fetch;
window.fetch = function() {
console.log("A fetch happened!");
return originalFetch.apply(window, arguments);
}
// Later somewhere in the application
window.fetch('https://example.com');
// Will show the console log
1
2
3
4
5
6
7
8
9
"Monkey patching is a technique used to dynamically
update the behavior of a piece of code at run-time."
11
- 12. Know your Use Case
Expect the Unexpected
How to Optimize Bundle Size
How to Compress Data
Lessons Learned
12
- 14. Know your Use Case
What do you really need?
Hide text & user input by default
Opt-in to show certain text
Defaults matter: Make the best
way the easy way
Make the worst things impossible
14
- 16. Expect the Unexpected
Low Level APIs are dangerous to tinker with
Monkey Patching is dangerous
Browser Extensions can do anything
try-catch everything
16
- 18. Session Replay Bundle Size
Bundle Size v7.73.0: ~53 KB
Bundle Size v7.78.0: ~35 KB
Still large 😱
... but how?
18
- 20. What is Tree Shaking?
describes the ability to
automatically remove unused code from
your build.
When code is written in a tree-shakeable
way, bundlers like Webpack can optimize
your application based on what is actually
used.
Tree Shaking
20
- 21. Tree Shaking: Simple Example
// SDK
import { large, small } from './my-code';
export function largeOrSmall(config) {
return config.useLage ? large() : small();
}
// Application
import { largeOrSmall } from 'sdk';
largeOrSmall({ useLarge: false });
1
2
3
4
5
6
7
8
9
10
11
❌Not tree shakeable
21
- 22. // SDK
export { large, small } from './my-code';
// Application
import { small } from 'sdk';
small();
1
2
3
4
5
6
7
✅Tree shakeable
Tree Shaking: Simple Example
22
- 23. // SDK
import {
CanvasManager
} from './canvas-manager';
export function record(options) {
if (options.recordCanvas) {
new CanvasManager();
}
}
// Application
import { record } from 'sdk';
record({ recordCanvas: false });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❌Not tree shakeable
Tree Shaking: Actual Example
23
- 24. // Application A
import {
record
} from 'sdk';
record({ getCanvasManager: undefined });
1
2
3
4
5
6
✅Tree shakeable
Tree Shaking: Actual Example
// SDK
import {
CanvasManager
} from './canvas-manager';
export function getCanvasManager() {
return new CanvasManager();
}
export function record(options) {
if (options.getCanvasManager) {
options.getCanvasManager();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Application B
import {
record,
getCanvasManager
} from 'sdk';
record({ getCanvasManager });
1
2
3
4
5
6
7
24
- 25. How to Compress Data
Avoid unnecessary network traffic, where possible.
25
- 27. Web Workers
Setting up the web worker
// worker.js
import { compressSync } from 'fflate';
function handleMessage(e) {
const { input, id } = e.data;
const compressed = compressSync(input);
// Send compressed data back to main thread
postMessage({ id, output: compressed });
}
// Receive uncompressed data from main thread
addEventListener('message', handleMessage);
1
2
3
4
5
6
7
8
9
10
11
12
27
- 28. Web Workers
Using the web worker from your application
// Application
const worker = new Worker('/worker.js');
function compressData(data) {
const id = generateUuid();
return new Promise(function (resolve) {
function listener(response) {
if (response.data.id === id) {
worker.removeEventListener('message', listener);
resolve(response.data.output);
}
}
worker.addEventListener('message', listener);
worker.postMessage({ id, input: data });
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
28