Skip to content

Commit 7256255

Browse files
committed
ref: Extract worker into package
1 parent c3806eb commit 7256255

17 files changed

+401
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"packages/react",
5252
"packages/remix",
5353
"packages/replay",
54+
"packages/replay-worker",
5455
"packages/serverless",
5556
"packages/svelte",
5657
"packages/tracing",

packages/replay-worker/.eslintrc.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Note: All paths are relative to the directory in which eslint is being run, rather than the directory where this file
2+
// lives
3+
4+
// ESLint config docs: https://eslint.org/docs/user-guide/configuring/
5+
6+
module.exports = {
7+
extends: ['../../.eslintrc.js'],
8+
overrides: [
9+
{
10+
files: ['src/**/*.ts'],
11+
rules: {
12+
// We cannot use backticks, as that conflicts with the stringified worker
13+
'prefer-template': 'off',
14+
},
15+
},
16+
],
17+
};

packages/replay-worker/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
/*.tgz
3+
.eslintcache
4+
build
5+
!types/*.d.ts

packages/replay-worker/LICENSE

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Copyright (c) 2022 Sentry (https://sentry.io) and individual contributors. All rights reserved.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
5+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
6+
persons to whom the Software is furnished to do so, subject to the following conditions:
7+
8+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
9+
Software.
10+
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
12+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
13+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
14+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

packages/replay-worker/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<p align="center">
2+
<a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
3+
<img src="https://sentry-brand.storage.googleapis.com/sentry-wordmark-dark-280x84.png" alt="Sentry" width="280" height="84">
4+
</a>
5+
</p>
6+
7+
# Sentry Session Replay Worker
8+
9+
This is an internal package that is used by @sentry/replay.
10+
It generates a web worker and converts it to a string, so that we can process it easier in replay.
11+
12+
By extracting this into a dedicated (private, internal) package, we can streamline the build of replay.

packages/replay-worker/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('../../jest/jest.config.js');

packages/replay-worker/package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@sentry-internal/replay-worker",
3+
"version": "7.37.0",
4+
"description": "Worker for @sentry/replay",
5+
"main": "build/index.js",
6+
"module": "build/index.js",
7+
"sideEffects": false,
8+
"private": true,
9+
"scripts": {
10+
"build": "yarn build:transpile",
11+
"build:transpile": "rollup -c rollup.worker.config.js",
12+
"build:types": "yarn build:transpile",
13+
"build:dev": "yarn build",
14+
"build:watch": "run-p build:transpile:watch",
15+
"build:dev:watch": "run-p build:watch",
16+
"build:transpile:watch": "yarn build:rollup --watch",
17+
"circularDepCheck": "madge --circular src/index.ts",
18+
"clean": "rimraf build",
19+
"fix": "run-s fix:eslint fix:prettier",
20+
"fix:eslint": "eslint . --format stylish --fix",
21+
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
22+
"lint": "run-s lint:prettier lint:eslint",
23+
"lint:eslint": "eslint . --format stylish",
24+
"lint:prettier": "prettier --check \"{src,test}/**/*.ts\"",
25+
"test": "jest",
26+
"test:watch": "jest --watch"
27+
},
28+
"repository": {
29+
"type": "git",
30+
"url": "git+https://github.com/getsentry/sentry-javascript.git"
31+
},
32+
"author": "Sentry",
33+
"license": "MIT",
34+
"bugs": {
35+
"url": "https://github.com/getsentry/sentry-javascript/issues"
36+
},
37+
"homepage": "https://docs.sentry.io/platforms/javascript/session-replay/",
38+
"devDependencies": {
39+
"@types/pako": "^2.0.0",
40+
"rollup-plugin-copy": "~3.4.0",
41+
"tslib": "^1.9.3"
42+
},
43+
"dependencies": {
44+
"pako": "^2.1.0"
45+
},
46+
"engines": {
47+
"node": ">=12"
48+
},
49+
"volta": {
50+
"extends": "../../package.json"
51+
}
52+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// inspired by https://justinribeiro.com/chronicle/2020/07/17/building-module-web-workers-for-cross-browser-compatibility-with-rollup/
2+
3+
import resolve from '@rollup/plugin-node-resolve';
4+
import typescript from '@rollup/plugin-typescript';
5+
import { defineConfig } from 'rollup';
6+
import { terser } from 'rollup-plugin-terser';
7+
import copy from 'rollup-plugin-copy';
8+
9+
const config = defineConfig({
10+
input: ['./src/index.ts'],
11+
output: {
12+
dir: './build/',
13+
format: 'esm',
14+
},
15+
plugins: [
16+
typescript({ tsconfig: './tsconfig.json', inlineSourceMap: false, sourceMap: false, inlineSources: false }),
17+
resolve(),
18+
terser({
19+
mangle: {
20+
module: true,
21+
},
22+
}),
23+
{
24+
name: 'worker-to-string',
25+
renderChunk(code) {
26+
return `export default \`${code}\`;`;
27+
},
28+
},
29+
copy({
30+
targets: [{ src: 'types/*', dest: 'build' }],
31+
}),
32+
],
33+
});
34+
35+
export default config;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { constants, Deflate, deflate } from 'pako';
2+
3+
/**
4+
* A stateful compressor that can be used to batch compress events.
5+
*/
6+
export class Compressor {
7+
/**
8+
* pako deflator instance
9+
*/
10+
public deflate: Deflate;
11+
12+
/**
13+
* If any events have been added.
14+
*/
15+
private _hasEvents: boolean;
16+
17+
public constructor() {
18+
this._init();
19+
}
20+
21+
/**
22+
* Clear the compressor buffer.
23+
*/
24+
public clear(): void {
25+
this._init();
26+
}
27+
28+
/**
29+
* Add an event to the compressor buffer.
30+
*/
31+
public addEvent(data: string): void {
32+
if (!data) {
33+
throw new Error('Adding invalid event');
34+
}
35+
// If the event is not the first event, we need to prefix it with a `,` so
36+
// that we end up with a list of events
37+
const prefix = this._hasEvents ? ',' : '';
38+
// TODO: We may want Z_SYNC_FLUSH or Z_FULL_FLUSH (not sure the difference)
39+
// Using NO_FLUSH here for now as we can create many attachments that our
40+
// web UI will get API rate limited.
41+
this.deflate.push(prefix + data, constants.Z_SYNC_FLUSH);
42+
43+
this._hasEvents = true;
44+
}
45+
46+
/**
47+
* Finish compression of the current buffer.
48+
*/
49+
public finish(): Uint8Array {
50+
// We should always have a list, it can be empty
51+
this.deflate.push(']', constants.Z_FINISH);
52+
53+
if (this.deflate.err) {
54+
throw this.deflate.err;
55+
}
56+
57+
// Copy result before we create a new deflator and return the compressed
58+
// result
59+
const result = this.deflate.result;
60+
61+
this._init();
62+
63+
return result;
64+
}
65+
66+
/**
67+
* Re-initialize the compressor buffer.
68+
*/
69+
private _init(): void {
70+
this._hasEvents = false;
71+
this.deflate = new Deflate();
72+
73+
// Fake an array by adding a `[`
74+
this.deflate.push('[', constants.Z_NO_FLUSH);
75+
}
76+
}
77+
78+
/**
79+
* Compress a string.
80+
*/
81+
export function compress(data: string): Uint8Array {
82+
return deflate(data);
83+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2+
import { compress, Compressor } from './Compressor';
3+
4+
const compressor = new Compressor();
5+
6+
interface Handlers {
7+
clear: () => void;
8+
addEvent: (data: string) => void;
9+
finish: () => Uint8Array;
10+
compress: (data: string) => Uint8Array;
11+
}
12+
13+
const handlers: Handlers = {
14+
clear: () => {
15+
compressor.clear();
16+
},
17+
18+
addEvent: (data: string) => {
19+
compressor.addEvent(data);
20+
},
21+
22+
finish: () => {
23+
return compressor.finish();
24+
},
25+
26+
compress: (data: string) => {
27+
return compress(data);
28+
},
29+
};
30+
31+
/**
32+
* Handler for worker messages.
33+
*/
34+
export function handleMessage(e: MessageEvent): void {
35+
const method = e.data.method as string;
36+
const id = e.data.id as number;
37+
const data = e.data.arg as string;
38+
39+
// @ts-ignore this syntax is actually fine
40+
if (method in handlers && typeof handlers[method] === 'function') {
41+
try {
42+
// @ts-ignore this syntax is actually fine
43+
const response = handlers[method](data);
44+
// @ts-ignore this syntax is actually fine
45+
postMessage({
46+
id,
47+
method,
48+
success: true,
49+
response,
50+
});
51+
} catch (err) {
52+
// @ts-ignore this syntax is actually fine
53+
postMessage({
54+
id,
55+
method,
56+
success: false,
57+
response: err.message,
58+
});
59+
60+
// eslint-disable-next-line no-console
61+
console.error(err);
62+
}
63+
}
64+
}

packages/replay-worker/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { handleMessage } from './handleMessage';
2+
3+
addEventListener('message', handleMessage);
4+
5+
// Immediately send a message when worker loads, so we know the worker is ready
6+
// @ts-ignore this syntax is actually fine
7+
postMessage({
8+
id: undefined,
9+
method: 'init',
10+
success: true,
11+
response: undefined,
12+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// TODO Once https://github.com/microsoft/TypeScript/issues/33094 is done (if it ever is), this file can disappear, as
2+
// it's purely a placeholder to satisfy VSCode.
3+
{
4+
"extends": "../tsconfig.test.json",
5+
6+
"include": ["./**/*"]
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pako from 'pako';
2+
3+
import { Compressor } from '../../src/Compressor';
4+
5+
describe('Compressor', () => {
6+
it('compresses multiple events', () => {
7+
const compressor = new Compressor();
8+
9+
const events = [
10+
{
11+
id: 1,
12+
foo: ['bar', 'baz'],
13+
},
14+
{
15+
id: 2,
16+
foo: [false],
17+
},
18+
];
19+
20+
events.forEach(event => compressor.addEvent(JSON.stringify(event)));
21+
22+
const compressed = compressor.finish();
23+
24+
const restored = pako.inflate(compressed, { to: 'string' });
25+
26+
expect(restored).toBe(JSON.stringify(events));
27+
});
28+
29+
it('throws on invalid/undefined events', () => {
30+
const compressor = new Compressor();
31+
32+
// @ts-ignore ignoring type for test
33+
expect(() => void compressor.addEvent(undefined)).toThrow();
34+
35+
const compressed = compressor.finish();
36+
37+
const restored = pako.inflate(compressed, { to: 'string' });
38+
39+
expect(restored).toBe(JSON.stringify([]));
40+
});
41+
});

packages/replay-worker/tsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"module": "esnext",
5+
"lib": ["webworker", "scripthost"],
6+
"esModuleInterop": true,
7+
"target": "es6",
8+
"strictPropertyInitialization": false
9+
},
10+
"include": ["src/**/*.ts"]
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["test/**/*.ts"],
4+
"compilerOptions": {
5+
"types": ["node", "jest"]
6+
}
7+
}

0 commit comments

Comments
 (0)