comments
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__acp__Bash",
|
||||
"mcp__acp__Edit",
|
||||
"mcp__gsc__gsc_pages_stats",
|
||||
"mcp__gsc__gsc_queries_list",
|
||||
"mcp__gsc__gsc_pages_list",
|
||||
"mcp__gsc__gsc_queries_stats",
|
||||
"mcp__gsc__gsc_queries_opportunities",
|
||||
"mcp__gsc__spyfu_domain_compare",
|
||||
"mcp__gsc__spyfu_seo_keywords",
|
||||
"mcp__gsc__gsc_devices_list",
|
||||
"mcp__acp__Write",
|
||||
"mcp__gsc__spyfu_serp_analysis",
|
||||
"mcp__gsc__spyfu_keyword_info",
|
||||
"mcp__gsc__serpapi_search",
|
||||
"mcp__searchapi__google_rank_tracking",
|
||||
"WebFetch(domain:www.mparticle.com)",
|
||||
"WebFetch(domain:posthog.com)",
|
||||
"WebFetch(domain:openpanel.dev)",
|
||||
"mcp__gsc__gsc_sites_list",
|
||||
"WebFetch(domain:plausible.io)",
|
||||
"WebFetch(domain:opennext.js.org)",
|
||||
"WebFetch(domain:clickhouse.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ async function handleTrack(
|
||||
.filter(Boolean)
|
||||
.join('-');
|
||||
|
||||
const promises = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// If we have more than one property in the identity object, we should identify the user
|
||||
// Otherwise its only a profileId and we should not identify the user
|
||||
@@ -436,7 +436,7 @@ export async function fetchDeviceId(
|
||||
const data = JSON.parse(res?.[0]?.[1] as string);
|
||||
const sessionId = data.payload.sessionId;
|
||||
return reply.status(200).send({
|
||||
deviceId: sessionId,
|
||||
deviceId: currentDeviceId,
|
||||
sessionId,
|
||||
message: 'current session exists for this device id',
|
||||
});
|
||||
@@ -446,7 +446,7 @@ export async function fetchDeviceId(
|
||||
const data = JSON.parse(res?.[1]?.[1] as string);
|
||||
const sessionId = data.payload.sessionId;
|
||||
return reply.status(200).send({
|
||||
deviceId: sessionId,
|
||||
deviceId: previousDeviceId,
|
||||
sessionId,
|
||||
message: 'previous session exists for this device id',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { generateDeviceId } from '@openpanel/common/server';
|
||||
import { getSafeJson } from '@openpanel/json';
|
||||
import { getRedisCache } from '@openpanel/redis';
|
||||
|
||||
export async function getDeviceId({
|
||||
@@ -16,11 +17,11 @@ export async function getDeviceId({
|
||||
overrideDeviceId?: string;
|
||||
}) {
|
||||
if (overrideDeviceId) {
|
||||
return { deviceId: overrideDeviceId, sessionId: undefined };
|
||||
return { deviceId: overrideDeviceId, sessionId: '' };
|
||||
}
|
||||
|
||||
if (!ua) {
|
||||
return { deviceId: '', sessionId: undefined };
|
||||
return { deviceId: '', sessionId: '' };
|
||||
}
|
||||
|
||||
const currentDeviceId = generateDeviceId({
|
||||
@@ -56,22 +57,30 @@ async function getDeviceIdFromSession({
|
||||
const multi = getRedisCache().multi();
|
||||
multi.hget(
|
||||
`bull:sessions:sessionEnd:${projectId}:${currentDeviceId}`,
|
||||
'data',
|
||||
'data'
|
||||
);
|
||||
multi.hget(
|
||||
`bull:sessions:sessionEnd:${projectId}:${previousDeviceId}`,
|
||||
'data',
|
||||
'data'
|
||||
);
|
||||
const res = await multi.exec();
|
||||
if (res?.[0]?.[1]) {
|
||||
const data = JSON.parse(res?.[0]?.[1] as string);
|
||||
const sessionId = data.payload.sessionId;
|
||||
return { deviceId: currentDeviceId, sessionId };
|
||||
const data = getSafeJson<{ payload: { sessionId: string } }>(
|
||||
(res?.[0]?.[1] as string) ?? ''
|
||||
);
|
||||
if (data) {
|
||||
const sessionId = data.payload.sessionId;
|
||||
return { deviceId: currentDeviceId, sessionId };
|
||||
}
|
||||
}
|
||||
if (res?.[1]?.[1]) {
|
||||
const data = JSON.parse(res?.[1]?.[1] as string);
|
||||
const sessionId = data.payload.sessionId;
|
||||
return { deviceId: previousDeviceId, sessionId };
|
||||
const data = getSafeJson<{ payload: { sessionId: string } }>(
|
||||
(res?.[1]?.[1] as string) ?? ''
|
||||
);
|
||||
if (data) {
|
||||
const sessionId = data.payload.sessionId;
|
||||
return { deviceId: previousDeviceId, sessionId };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting session end GET /track/device-id', error);
|
||||
@@ -113,13 +122,21 @@ function getSessionId(params: {
|
||||
bytes = 16,
|
||||
} = params;
|
||||
|
||||
if (!projectId) throw new Error('projectId is required');
|
||||
if (!deviceId) throw new Error('deviceId is required');
|
||||
if (windowMs <= 0) throw new Error('windowMs must be > 0');
|
||||
if (graceMs < 0 || graceMs >= windowMs)
|
||||
if (!projectId) {
|
||||
throw new Error('projectId is required');
|
||||
}
|
||||
if (!deviceId) {
|
||||
throw new Error('deviceId is required');
|
||||
}
|
||||
if (windowMs <= 0) {
|
||||
throw new Error('windowMs must be > 0');
|
||||
}
|
||||
if (graceMs < 0 || graceMs >= windowMs) {
|
||||
throw new Error('graceMs must be >= 0 and < windowMs');
|
||||
if (bytes < 8 || bytes > 32)
|
||||
}
|
||||
if (bytes < 8 || bytes > 32) {
|
||||
throw new Error('bytes must be between 8 and 32');
|
||||
}
|
||||
|
||||
const bucket = Math.floor(eventMs / windowMs);
|
||||
const offset = eventMs - bucket * windowMs;
|
||||
|
||||
@@ -33,7 +33,7 @@ This is the most common flow and most secure one. Your backend receives webhooks
|
||||
<FlowStep step={2} actor="Visitor" description="Makes a purchase" icon="visitor" />
|
||||
|
||||
<FlowStep step={3} actor="Your website" description="Does a POST request to get the checkout URL" icon="website">
|
||||
When you create the checkout, you should first call `op.fetchDeviceId()`, which will return your visitor's current `deviceId`. Pass this to your checkout endpoint.
|
||||
When you create the checkout, you should first call `op.getDeviceId()`, which will return your visitor's current `deviceId`. Pass this to your checkout endpoint.
|
||||
|
||||
```javascript
|
||||
fetch('https://domain.com/api/checkout', {
|
||||
@@ -42,7 +42,7 @@ fetch('https://domain.com/api/checkout', {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId: await op.fetchDeviceId(), // ✅ since deviceId is here we can link the payment now
|
||||
deviceId: op.getDeviceId(), // ✅ since deviceId is here we can link the payment now
|
||||
// ... other checkout data
|
||||
}),
|
||||
})
|
||||
@@ -360,5 +360,5 @@ op.clearRevenue(): void
|
||||
### Fetch your current users device id
|
||||
|
||||
```javascript
|
||||
op.fetchDeviceId(): Promise<string>
|
||||
op.getDeviceId(): string
|
||||
```
|
||||
|
||||
@@ -136,7 +136,7 @@ For more accurate tracking, handle revenue in your backend webhook. This ensures
|
||||
|
||||
```tsx
|
||||
// Frontend: include deviceId when starting checkout
|
||||
const deviceId = await op.fetchDeviceId();
|
||||
const deviceId = op.getDeviceId();
|
||||
|
||||
const response = await fetch('/api/checkout', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
(() => {
|
||||
var u = class {
|
||||
constructor(e) {
|
||||
(this.baseUrl = e.baseUrl),
|
||||
(this.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...e.defaultHeaders,
|
||||
}),
|
||||
(this.maxRetries = e.maxRetries ?? 3),
|
||||
(this.initialRetryDelay = e.initialRetryDelay ?? 500);
|
||||
}
|
||||
async resolveHeaders() {
|
||||
const e = {};
|
||||
for (const [i, t] of Object.entries(this.headers)) {
|
||||
const s = await t;
|
||||
s !== null && (e[i] = s);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
addHeader(e, i) {
|
||||
this.headers[e] = i;
|
||||
}
|
||||
async post(e, i, t, s) {
|
||||
try {
|
||||
const n = await fetch(e, {
|
||||
method: 'POST',
|
||||
headers: await this.resolveHeaders(),
|
||||
body: i ? JSON.stringify(i ?? {}) : void 0,
|
||||
keepalive: !0,
|
||||
...t,
|
||||
});
|
||||
if (n.status === 401) {
|
||||
return null;
|
||||
}
|
||||
if (n.status !== 200 && n.status !== 202) {
|
||||
throw new Error(`HTTP error! status: ${n.status}`);
|
||||
}
|
||||
const r = await n.text();
|
||||
return r ? JSON.parse(r) : null;
|
||||
} catch (n) {
|
||||
if (s < this.maxRetries) {
|
||||
const r = this.initialRetryDelay * 2 ** s;
|
||||
return (
|
||||
await new Promise((a) => setTimeout(a, r)),
|
||||
this.post(e, i, t, s + 1)
|
||||
);
|
||||
}
|
||||
return console.error('Max retries reached:', n), null;
|
||||
}
|
||||
}
|
||||
async fetch(e, i, t = {}) {
|
||||
const s = `${this.baseUrl}${e}`;
|
||||
return this.post(s, i, t, 0);
|
||||
}
|
||||
},
|
||||
l = class {
|
||||
constructor(e) {
|
||||
(this.options = e), (this.queue = []);
|
||||
const i = { 'openpanel-client-id': e.clientId };
|
||||
e.clientSecret && (i['openpanel-client-secret'] = e.clientSecret),
|
||||
(i['openpanel-sdk-name'] = e.sdk || 'node'),
|
||||
(i['openpanel-sdk-version'] = e.sdkVersion || '1.0.3'),
|
||||
(this.api = new u({
|
||||
baseUrl: e.apiUrl || 'https://api.openpanel.dev',
|
||||
defaultHeaders: i,
|
||||
}));
|
||||
}
|
||||
init() {}
|
||||
ready() {
|
||||
(this.options.waitForProfile = !1), this.flush();
|
||||
}
|
||||
async send(e) {
|
||||
return this.options.disabled ||
|
||||
(this.options.filter && !this.options.filter(e))
|
||||
? Promise.resolve()
|
||||
: this.options.waitForProfile && !this.profileId
|
||||
? (this.queue.push(e), Promise.resolve())
|
||||
: this.api.fetch('/track', e);
|
||||
}
|
||||
setGlobalProperties(e) {
|
||||
this.global = { ...this.global, ...e };
|
||||
}
|
||||
async track(e, i) {
|
||||
return (
|
||||
this.log('track event', e, i),
|
||||
this.send({
|
||||
type: 'track',
|
||||
payload: {
|
||||
name: e,
|
||||
profileId: i?.profileId ?? this.profileId,
|
||||
properties: { ...(this.global ?? {}), ...(i ?? {}) },
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
async identify(e) {
|
||||
if (
|
||||
(this.log('identify user', e),
|
||||
e.profileId && ((this.profileId = e.profileId), this.flush()),
|
||||
Object.keys(e).length > 1)
|
||||
) {
|
||||
return this.send({
|
||||
type: 'identify',
|
||||
payload: { ...e, properties: { ...this.global, ...e.properties } },
|
||||
});
|
||||
}
|
||||
}
|
||||
async alias(e) {}
|
||||
async increment(e) {
|
||||
return this.send({ type: 'increment', payload: e });
|
||||
}
|
||||
async decrement(e) {
|
||||
return this.send({ type: 'decrement', payload: e });
|
||||
}
|
||||
async revenue(e, i) {
|
||||
const t = i?.deviceId;
|
||||
return (
|
||||
delete i?.deviceId,
|
||||
this.track('revenue', {
|
||||
...(i ?? {}),
|
||||
...(t ? { __deviceId: t } : {}),
|
||||
__revenue: e,
|
||||
})
|
||||
);
|
||||
}
|
||||
async fetchDeviceId() {
|
||||
return (
|
||||
(
|
||||
await this.api.fetch('/track/device-id', void 0, {
|
||||
method: 'GET',
|
||||
keepalive: !1,
|
||||
})
|
||||
)?.deviceId ?? ''
|
||||
);
|
||||
}
|
||||
clear() {
|
||||
this.profileId = void 0;
|
||||
}
|
||||
flush() {
|
||||
this.queue.forEach((e) => {
|
||||
this.send({
|
||||
...e,
|
||||
payload: {
|
||||
...e.payload,
|
||||
profileId: e.payload.profileId ?? this.profileId,
|
||||
},
|
||||
});
|
||||
}),
|
||||
(this.queue = []);
|
||||
}
|
||||
log(...e) {
|
||||
this.options.debug && console.log('[OpenPanel.dev]', ...e);
|
||||
}
|
||||
};
|
||||
function h(e) {
|
||||
return e.replace(/([-_][a-z])/gi, (i) =>
|
||||
i.toUpperCase().replace('-', '').replace('_', '')
|
||||
);
|
||||
}
|
||||
var p = class extends l {
|
||||
constructor(t) {
|
||||
super({ sdk: 'web', sdkVersion: '1.0.6', ...t });
|
||||
this.options = t;
|
||||
this.lastPath = '';
|
||||
this.pendingRevenues = [];
|
||||
if (!this.isServer()) {
|
||||
try {
|
||||
const s = sessionStorage.getItem('openpanel-pending-revenues');
|
||||
if (s) {
|
||||
const n = JSON.parse(s);
|
||||
Array.isArray(n) && (this.pendingRevenues = n);
|
||||
}
|
||||
} catch {
|
||||
this.pendingRevenues = [];
|
||||
}
|
||||
this.setGlobalProperties({ __referrer: document.referrer }),
|
||||
this.options.trackScreenViews &&
|
||||
(this.trackScreenViews(), setTimeout(() => this.screenView(), 0)),
|
||||
this.options.trackOutgoingLinks && this.trackOutgoingLinks(),
|
||||
this.options.trackAttributes && this.trackAttributes();
|
||||
}
|
||||
}
|
||||
debounce(t, s) {
|
||||
clearTimeout(this.debounceTimer), (this.debounceTimer = setTimeout(t, s));
|
||||
}
|
||||
isServer() {
|
||||
return typeof document > 'u';
|
||||
}
|
||||
trackOutgoingLinks() {
|
||||
this.isServer() ||
|
||||
document.addEventListener('click', (t) => {
|
||||
const s = t.target,
|
||||
n = s.closest('a');
|
||||
if (n && s) {
|
||||
const r = n.getAttribute('href');
|
||||
if (r?.startsWith('http')) {
|
||||
try {
|
||||
const a = new URL(r),
|
||||
o = window.location.hostname;
|
||||
a.hostname !== o &&
|
||||
super.track('link_out', {
|
||||
href: r,
|
||||
text:
|
||||
n.innerText ||
|
||||
n.getAttribute('title') ||
|
||||
s.getAttribute('alt') ||
|
||||
s.getAttribute('title'),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
trackScreenViews() {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
const t = history.pushState;
|
||||
history.pushState = function (...a) {
|
||||
const o = t.apply(this, a);
|
||||
return (
|
||||
window.dispatchEvent(new Event('pushstate')),
|
||||
window.dispatchEvent(new Event('locationchange')),
|
||||
o
|
||||
);
|
||||
};
|
||||
const s = history.replaceState;
|
||||
(history.replaceState = function (...a) {
|
||||
const o = s.apply(this, a);
|
||||
return (
|
||||
window.dispatchEvent(new Event('replacestate')),
|
||||
window.dispatchEvent(new Event('locationchange')),
|
||||
o
|
||||
);
|
||||
}),
|
||||
window.addEventListener('popstate', () => {
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
});
|
||||
const n = () => this.debounce(() => this.screenView(), 50);
|
||||
this.options.trackHashChanges
|
||||
? window.addEventListener('hashchange', n)
|
||||
: window.addEventListener('locationchange', n);
|
||||
}
|
||||
trackAttributes() {
|
||||
this.isServer() ||
|
||||
document.addEventListener('click', (t) => {
|
||||
const s = t.target,
|
||||
n = s.closest('button'),
|
||||
r = s.closest('a'),
|
||||
a = n?.getAttribute('data-track')
|
||||
? n
|
||||
: r?.getAttribute('data-track')
|
||||
? r
|
||||
: null;
|
||||
if (a) {
|
||||
const o = {};
|
||||
for (const c of a.attributes) {
|
||||
c.name.startsWith('data-') &&
|
||||
c.name !== 'data-track' &&
|
||||
(o[h(c.name.replace(/^data-/, ''))] = c.value);
|
||||
}
|
||||
const d = a.getAttribute('data-track');
|
||||
d && super.track(d, o);
|
||||
}
|
||||
});
|
||||
}
|
||||
screenView(t, s) {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
let n, r;
|
||||
typeof t == 'string'
|
||||
? ((n = t), (r = s))
|
||||
: ((n = window.location.href), (r = t)),
|
||||
this.lastPath !== n &&
|
||||
((this.lastPath = n),
|
||||
super.track('screen_view', {
|
||||
...(r ?? {}),
|
||||
__path: n,
|
||||
__title: document.title,
|
||||
}));
|
||||
}
|
||||
async flushRevenue() {
|
||||
const t = this.pendingRevenues.map((s) =>
|
||||
super.revenue(s.amount, s.properties)
|
||||
);
|
||||
await Promise.all(t), this.clearRevenue();
|
||||
}
|
||||
clearRevenue() {
|
||||
if (((this.pendingRevenues = []), !this.isServer())) {
|
||||
try {
|
||||
sessionStorage.removeItem('openpanel-pending-revenues');
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
pendingRevenue(t, s) {
|
||||
if (
|
||||
(this.pendingRevenues.push({ amount: t, properties: s }),
|
||||
!this.isServer())
|
||||
) {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
'openpanel-pending-revenues',
|
||||
JSON.stringify(this.pendingRevenues)
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
};
|
||||
((e) => {
|
||||
if (e.op) {
|
||||
const i = e.op.q || [],
|
||||
t = new p(i.shift()[1]);
|
||||
i.forEach((n) => {
|
||||
n[0] in t && t[n[0]](...n.slice(1));
|
||||
});
|
||||
const s = new Proxy(
|
||||
(n, ...r) => {
|
||||
const a = t[n] ? t[n].bind(t) : void 0;
|
||||
typeof a == 'function'
|
||||
? a(...r)
|
||||
: console.warn(`OpenPanel: ${n} is not a function`);
|
||||
},
|
||||
{
|
||||
get(n, r) {
|
||||
if (r === 'q') {
|
||||
return;
|
||||
}
|
||||
const a = t[r];
|
||||
return typeof a == 'function' ? a.bind(t) : a;
|
||||
},
|
||||
}
|
||||
);
|
||||
(e.op = s), (e.openpanel = t);
|
||||
}
|
||||
})(window);
|
||||
})();
|
||||
@@ -93,6 +93,9 @@ export function ReplayPlayer({
|
||||
|
||||
playerRef.current = player;
|
||||
|
||||
// Track play state from replayer (getMetaData() does not expose isPlaying)
|
||||
let playingState = false;
|
||||
|
||||
// Wire rrweb's built-in event emitter — no RAF loop needed.
|
||||
// Note: rrweb-player does NOT emit ui-update-duration; duration is
|
||||
// read from getMetaData() on init and after each addEvent batch.
|
||||
@@ -102,16 +105,19 @@ export function ReplayPlayer({
|
||||
});
|
||||
|
||||
player.addEventListener('ui-update-player-state', (e) => {
|
||||
setIsPlaying(e.payload === 'playing');
|
||||
const playing = e.payload === 'playing';
|
||||
playingState = playing;
|
||||
setIsPlaying(playing);
|
||||
});
|
||||
|
||||
// Pause on tab hide; resume on show (prevents timer drift)
|
||||
// Pause on tab hide; resume on show (prevents timer drift).
|
||||
// getMetaData() does not expose isPlaying, so we use playingState
|
||||
// kept in sync by ui-update-player-state above.
|
||||
let wasPlaying = false;
|
||||
handleVisibilityChange = () => {
|
||||
if (!player) return;
|
||||
if (document.hidden) {
|
||||
const meta = player.getMetaData() as { isPlaying?: boolean };
|
||||
wasPlaying = meta.isPlaying ?? false;
|
||||
wasPlaying = playingState;
|
||||
if (wasPlaying) player.pause();
|
||||
} else {
|
||||
if (wasPlaying) {
|
||||
|
||||
@@ -56,6 +56,9 @@ export function ReplayTimeline({ events }: { events: IServiceEvent[] }) {
|
||||
(clientX: number) => {
|
||||
if (!trackRef.current || duration <= 0) return null;
|
||||
const rect = trackRef.current.getBoundingClientRect();
|
||||
if (rect.width <= 0 || !Number.isFinite(rect.width)) {
|
||||
return null;
|
||||
}
|
||||
const x = clientX - rect.left;
|
||||
const pct = Math.max(0, Math.min(1, x / rect.width));
|
||||
return { pct, timeMs: pct * duration };
|
||||
|
||||
@@ -136,6 +136,7 @@ export function useOpenPanel() {
|
||||
clearRevenue,
|
||||
pendingRevenue,
|
||||
fetchDeviceId,
|
||||
getDeviceId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,6 +172,9 @@ function decrement(payload: DecrementPayload) {
|
||||
function fetchDeviceId() {
|
||||
return window.op.fetchDeviceId();
|
||||
}
|
||||
function getDeviceId() {
|
||||
return window.op.getDeviceId();
|
||||
}
|
||||
function clearRevenue() {
|
||||
window.op.clearRevenue();
|
||||
}
|
||||
|
||||
@@ -48,8 +48,9 @@ export function startReplayRecorder(
|
||||
if (buffer.length === 0) return;
|
||||
|
||||
const payloadJson = JSON.stringify(buffer);
|
||||
const payloadBytes = new TextEncoder().encode(payloadJson).length;
|
||||
|
||||
if (payloadJson.length > maxPayloadBytes) {
|
||||
if (payloadBytes > maxPayloadBytes) {
|
||||
if (buffer.length > 1) {
|
||||
const mid = Math.floor(buffer.length / 2);
|
||||
const firstHalf = buffer.slice(0, mid);
|
||||
@@ -70,17 +71,21 @@ export function startReplayRecorder(
|
||||
const startedAt = buffer[0]!.timestamp;
|
||||
const endedAt = buffer[buffer.length - 1]!.timestamp;
|
||||
|
||||
sendChunk({
|
||||
chunk_index: chunkIndex,
|
||||
events_count: buffer.length,
|
||||
is_full_snapshot: isFullSnapshot,
|
||||
started_at: new Date(startedAt).toISOString(),
|
||||
ended_at: new Date(endedAt).toISOString(),
|
||||
payload: payloadJson,
|
||||
});
|
||||
|
||||
chunkIndex += 1;
|
||||
buffer = [];
|
||||
try {
|
||||
sendChunk({
|
||||
chunk_index: chunkIndex,
|
||||
events_count: buffer.length,
|
||||
is_full_snapshot: isFullSnapshot,
|
||||
started_at: new Date(startedAt).toISOString(),
|
||||
ended_at: new Date(endedAt).toISOString(),
|
||||
payload: payloadJson,
|
||||
});
|
||||
chunkIndex += 1;
|
||||
buffer = [];
|
||||
} catch (err) {
|
||||
console.error('[ReplayRecorder] sendChunk failed', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function flushIfNeeded(isCheckout: boolean): void {
|
||||
@@ -132,6 +137,11 @@ export function startReplayRecorder(
|
||||
window.addEventListener('pagehide', onPageHide);
|
||||
|
||||
stopRecording = () => {
|
||||
// Flush any buffered events before tearing down (same logic as flushTimer)
|
||||
if (buffer.length > 0) {
|
||||
const hasFullSnapshot = buffer.some((e) => e.type === 2);
|
||||
flush(hasFullSnapshot);
|
||||
}
|
||||
if (flushTimer) {
|
||||
clearInterval(flushTimer);
|
||||
flushTimer = null;
|
||||
|
||||
6
packages/sdks/web/src/types.d.ts
vendored
6
packages/sdks/web/src/types.d.ts
vendored
@@ -14,7 +14,9 @@ type ExposedMethodsNames =
|
||||
| 'clearRevenue'
|
||||
| 'pendingRevenue'
|
||||
| 'screenView'
|
||||
| 'fetchDeviceId';
|
||||
| 'fetchDeviceId'
|
||||
| 'getDeviceId'
|
||||
| 'getSessionId';
|
||||
|
||||
export type ExposedMethods = {
|
||||
[K in ExposedMethodsNames]: OpenPanel[K] extends (...args: any[]) => any
|
||||
@@ -38,7 +40,7 @@ type OpenPanelMethodSignatures = {
|
||||
} & {
|
||||
screenView(
|
||||
pathOrProperties?: string | TrackProperties,
|
||||
properties?: TrackProperties,
|
||||
properties?: TrackProperties
|
||||
): void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Test callable function API
|
||||
/** biome-ignore-all lint/correctness/noUnusedVariables: test */
|
||||
function testCallableAPI() {
|
||||
// ✅ Should work - correct callable syntax
|
||||
window.op('track', 'button_clicked', { location: 'header' });
|
||||
@@ -29,6 +30,7 @@ function testDirectMethodAPI() {
|
||||
window.op.flushRevenue();
|
||||
window.op.clearRevenue();
|
||||
window.op.fetchDeviceId();
|
||||
window.op.getDeviceId();
|
||||
|
||||
// ❌ Should error - wrong arguments for track
|
||||
// @ts-expect-error - track expects (name: string, properties?: TrackProperties)
|
||||
|
||||
Reference in New Issue
Block a user