Skip to content

Commit

Permalink
feat: Add sidecar (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
HazAT authored Oct 2, 2023
1 parent 8361e46 commit 1d4c010
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 47 deletions.
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "sentry-spotlight",
"name": "@sentry/spotlight",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "tsc && vite build && yarn build:sidecar",
"build:sidecar": "tsc --module nodenext --moduleResolution nodenext --esModuleInterop false --target esnext src/node/sidecar.ts --outDir dist/",
"watch:build": "vite build --watch",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
Expand All @@ -18,6 +19,10 @@
".": {
"import": "./dist/sentry-spotlight.js",
"require": "./dist/sentry-spotlight.umd.cjs"
},
"./sidecar": {
"import": "./dist/sidecar.js",
"require": "./dist/sidecar.cjs"
}
},
"dependencies": {
Expand Down Expand Up @@ -47,5 +52,9 @@
"eslint-plugin-react-refresh": "^0.4.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
},
"volta": {
"node": "18.18.0",
"yarn": "1.22.19"
}
}
38 changes: 38 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function init({
} = {}) {
if (typeof document === "undefined") return;

hookIntoSentry();
connectToRelay(relay);

// build shadow dom container to contain styles
Expand Down Expand Up @@ -71,6 +72,43 @@ export function pushEnvelope(envelope: Envelope) {
dataCache.pushEnvelope(envelope);
}

function hookIntoSentry() {
// A very hacky way to hook into Sentry's SDK
// but we love hacks
(window as any).__SENTRY__.hub._stack[0].client.setupIntegrations(true);
(window as any).__SENTRY__.hub._stack[0].client.on("beforeEnvelope", (envelope: any) => {
fetch('http://localhost:8969/stream', {
method: 'POST',
body: serializeEnvelope(envelope),
headers: {
'Content-Type': 'application/x-sentry-envelope',
},
mode: 'cors',
})
.catch(err => {
console.error(err);
});
});
}

function serializeEnvelope(envelope: Envelope): string {
const [envHeaders, items] = envelope;

// Initially we construct our envelope as a string and only convert to binary chunks if we encounter binary data
const parts: string[] = [];
parts.push(JSON.stringify(envHeaders));

for (const item of items) {
const [itemHeaders, payload] = item;

parts.push(`\n${JSON.stringify(itemHeaders)}\n`);

parts.push(JSON.stringify(payload));
}

return parts.join("");
}

function connectToRelay(relay: string = DEFAULT_RELAY) {
console.log("[Spotlight] Connecting to relay");
const source = new EventSource(relay || DEFAULT_RELAY);
Expand Down
209 changes: 209 additions & 0 deletions src/node/sidecar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { Server, createServer } from "http";

const defaultResponse = `<!doctype html>
<html>
<head>
<title>pipe</title>
</head>
<body>
<pre id="output"></pre>
<script type="text/javascript">
const Output = document.getElementById("output");
var EvtSource = new EventSource('/stream');
EvtSource.onmessage = function (event) {
Output.appendChild(document.createTextNode(event.data));
Output.appendChild(document.createElement("br"));
};
</script>
</body>
</html>`;

function generate_uuidv4() {
let dt = new Date().getTime();
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
let rnd = Math.random() * 16;
rnd = (dt + rnd) % 16 | 0;
dt = Math.floor(dt / 16);
return (c === "x" ? rnd : (rnd & 0x3) | 0x8).toString(16);
});
}

class MessageBuffer<T> {
private _size: number;
private _items: [number, T][];
private _writePos: number = 0;
private _head: number = 0;
private _timeout: number = 10;
private _readers: Map<string, (item: T) => void>;

public constructor(size = 100) {
this._size = size;
this._items = new Array(size);
this._readers = new Map<string, (item: T) => void>();
}

public put(item: T): void {
const curTime = new Date().getTime();
this._items[this._writePos % this._size] = [curTime, item];
this._writePos += 1;
if (this._head === this._writePos) {
this._head += 1;
}

const minTime = curTime - this._timeout * 1000;
let atItem;
while (this._head < this._writePos) {
atItem = this._items[this._head % this._size];
if (atItem === undefined) break;
if (atItem[0] > minTime) break;
this._head += 1;
}
}

public subscribe(callback: (item: T) => void): string {
const readerId = generate_uuidv4();
this._readers.set(readerId, callback);
setTimeout(() => this.stream(readerId));
return readerId;
}

public unsubscribe(readerId: string): void {
this._readers.delete(readerId);
}

public stream(readerId: string, readPos?: number): void {
const cb = this._readers.get(readerId);
if (!cb) return;

let atReadPos = typeof readPos === "undefined" ? this._head : readPos;
let item;
while (true) {
item = this._items[atReadPos % this._size];
if (typeof item === "undefined") {
break;
}
cb(item[1]);
atReadPos += 1;
}
setTimeout(() => this.stream(readerId, atReadPos), 500);
}
}

const ENVELOPE = "envelope";
const EVENT = "event";

type Payload = [string, string];

let serverInstance: Server;

function getCorsHeader(): { [name: string]: string } {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "*",
};
}

function startServer(buffer: MessageBuffer<Payload>, port: number): Server {
const server = createServer((req, res) => {
console.log(`[spotlight] Received request ${req.method} ${req.url}`);
if (req.headers.accept && req.headers.accept == "text/event-stream") {
if (req.url == "/stream") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
...getCorsHeader(),
Connection: "keep-alive",
});
res.flushHeaders();

const sub = buffer.subscribe(([payloadType, data]) => {
res.write(`event:${payloadType}\n`);
data.split("\n").forEach((line) => {
res.write(`data:${line}\n`);
});
res.write("\n");
});

req.on("close", () => {
buffer.unsubscribe(sub);
});
} else {
res.writeHead(404);
res.end();
}
} else {
if (req.url == "/stream") {
if (req.method === "OPTIONS") {
res.writeHead(204, {
"Cache-Control": "no-cache",
...getCorsHeader(),
});
res.end();
} else if (req.method === "POST") {
let body: string = "";
req.on("readable", () => {
const chunk = req.read();
if (chunk !== null) body += chunk;
});
req.on("end", () => {
const payloadType =
req.headers["content-type"] === "application/x-sentry-envelope"
? ENVELOPE
: EVENT;
buffer.put([payloadType, body]);
res.writeHead(204, {
"Cache-Control": "no-cache",
...getCorsHeader(),
Connection: "keep-alive",
});
res.end();
});
} else {
res.writeHead(200, {
"Content-Type": "text/html",
});
res.write(defaultResponse);
res.end();
}
} else {
res.writeHead(404);
res.end();
}
}
});

server.on("error", (e) => {
if ("code" in e && e.code === "EADDRINUSE") {
// console.error('[Spotlight] Address in use, retrying...');
setTimeout(() => {
server.close();
server.listen(port);
}, 5000);
}
});
server.listen(port, () => {
console.log(`[Spotlight] Sidecar listening on ${port}`);
});

return server;
}

export function setupSidecar(): void {
const buffer: MessageBuffer<Payload> = new MessageBuffer<Payload>();

if (!serverInstance) {
serverInstance = startServer(buffer, 8969);
}
}

function shutdown() {
if (serverInstance) {
console.log("[Spotlight] Shutting down server");
serverInstance.close();
}
}

process.on("SIGTERM", () => {
shutdown();
});
Loading

0 comments on commit 1d4c010

Please sign in to comment.