This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-19 15:13:44 +01:00
parent 47adf46625
commit 41993d3463
35 changed files with 1098 additions and 233 deletions

View File

@@ -133,9 +133,8 @@ export class OpenPanel extends OpenPanelBase {
// string literal only in the IIFE build, so this branch is
// dead-code-eliminated in the library build.
if (typeof __OPENPANEL_REPLAY_URL__ !== 'undefined') {
// IIFE / script-tag context — load from CDN (or user override)
const url =
this.options.sessionReplay?.scriptUrl ?? __OPENPANEL_REPLAY_URL__;
const scriptEl = document.currentScript as HTMLScriptElement | null;
const url = this.options.sessionReplay?.scriptUrl || scriptEl?.src?.replace('.js', '-replay.js') || 'https://openpanel.dev/op1-replay.js';
// Already loaded (e.g. user included the script manually)
if ((window as any).__openpanel_replay) {

View File

@@ -42,15 +42,29 @@ export function startReplayRecorder(
function flush(isFullSnapshot: boolean): void {
if (buffer.length === 0) return;
const startedAt = buffer[0]!.timestamp;
const endedAt = buffer[buffer.length - 1]!.timestamp;
const payloadJson = JSON.stringify(buffer);
if (payloadJson.length > maxPayloadBytes) {
// If over size limit, split by taking only up to maxPayloadBytes (simplified: flush as-is and log or truncate)
// For MVP we still send; server will reject if over 1MB
if (buffer.length > 1) {
const mid = Math.floor(buffer.length / 2);
const firstHalf = buffer.slice(0, mid);
const secondHalf = buffer.slice(mid);
const firstHasFullSnapshot =
isFullSnapshot && firstHalf.some((e) => e.type === 2);
buffer = firstHalf;
flush(firstHasFullSnapshot);
buffer = secondHalf;
flush(false);
return;
}
// Single event exceeds limit — drop it to avoid server rejection
buffer = [];
return;
}
const startedAt = buffer[0]!.timestamp;
const endedAt = buffer[buffer.length - 1]!.timestamp;
sendChunk({
chunk_index: chunkIndex,
events_count: buffer.length,

View File

@@ -17,6 +17,8 @@ export default defineConfig([
// __OPENPANEL_REPLAY_URL__ is injected at build time so the IIFE
// knows to load the replay module from the CDN instead of a
// relative import (which doesn't work in a standalone script).
// The replay module is excluded via an esbuild plugin so it is
// never bundled into op1.js — it will be loaded lazily via <script>.
{
entry: { 'src/tracker': 'src/tracker.ts' },
format: ['iife'],
@@ -25,9 +27,30 @@ export default defineConfig([
minify: true,
define: {
__OPENPANEL_REPLAY_URL__: JSON.stringify(
'https://openpanel.dev/op1-replay.js',
'https://openpanel.dev/op1-replay.js'
),
},
esbuildPlugins: [
{
name: 'exclude-replay-from-iife',
setup(build) {
// Intercept any import that resolves to the replay module and
// return an empty object. The actual loading happens at runtime
// via a <script> tag (see loadReplayModule in index.ts).
build.onResolve(
{ filter: /[/\\]replay([/\\]index)?(\.[jt]s)?$/ },
() => ({
path: 'replay-empty-stub',
namespace: 'replay-stub',
})
);
build.onLoad({ filter: /.*/, namespace: 'replay-stub' }, () => ({
contents: 'module.exports = {}',
loader: 'js',
}));
},
},
],
},
// Replay module — built as both ESM (npm) and IIFE (CDN).
// ESM → consumed by the host-app's bundler via `import('./replay')`.