Files
zooqueenandzeekay 4712ee9d98 chore: complete posthog->insights rebrand (zero @posthog/* identity)
Decomplect the half-finished fork so the dependency graph has ONE source
with no posthog package identity, living under github.com/hanzoinsights.

LAYER 1 (package identity): rename straggler package.json names —
@posthog/next->@hanzo/insights-next (Next.js SDK; distinct from the existing
@hanzo/insights-nextjs config helper), @posthog/mcp->@hanzo/insights-mcp,
@posthog/plugin-utils->@hanzo/insights-plugin-utils,
@posthog/react-slim->@hanzo/react-slim, @posthog-tooling/release->
@hanzo/insights-tooling-release, and the @posthog/rrweb* family->@hanzo/rrweb*
(matching the already-published @hanzo/rrweb*@0.0.47 + minimumReleaseAgeExclude).
Repoint every repository.url/homepage/bugs to github.com/hanzoinsights/insights-js
and rebrand the PostHog author/description identity to Hanzo AI.

LAYER 2 (cross-package imports): rewrite every internal @posthog/* import to its
@hanzo/insights*|@hanzo/rrweb* equivalent and the bare module specifiers
posthog-js->@hanzo/insights, posthog-node->@hanzo/insights-node, mirrored in
package.json deps + the pnpm-lock importer keys. Result: 0 @posthog/* specifiers
in packages/tooling/examples/playground source.

LAYER 4 (wire protocol) PRESERVED: window.posthog global, ph_* keys,
$-prefixed event names, distinct_id, /decide, /e/, /flags, and the
__PosthogExtensions__ hook are left unchanged — the Insights server expects them.

Also repair pre-existing fork merge damage that blocked compilation (unrelated to
naming; identical to origin/main): restore the @hanzo/insights-core ./process
subpath (spawn-local/utils, re-matching the published artifact), the lost
constructor/var declarations in core + node clients, the mangled
external-scripts-loader, the missing TreeShakeable/isPlainError imports, the
DeviceId enum member, and stale ../posthog-core-stateless module paths. 26/34
turbo build tasks now pass; remaining failures (browser/react-native and their
dependents) are pre-existing fork damage, not rebrand-caused.
2026-07-16 09:09:17 -07:00

331 lines
9.9 KiB
TypeScript

import { expect } from 'vitest';
import {
NodeType,
EventType,
IncrementalSource,
eventWithTime,
Optional,
mouseInteractionData,
event,
pluginEvent,
} from '@hanzo/rrweb-types';
import * as puppeteer from 'puppeteer';
import * as path from 'path';
import * as http from 'http';
import * as url from 'url';
import * as fs from 'fs';
declare module 'puppeteer' {
interface Page {
waitForTimeout(ms: number): Promise<void>;
}
}
puppeteer.Page.prototype.waitForTimeout = function (ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export async function launchPuppeteer(
options?: Parameters<(typeof puppeteer)['launch']>[0],
) {
return await puppeteer.launch({
headless: process.env.PUPPETEER_HEADLESS ? 'new' : false,
defaultViewport: {
width: 1920,
height: 1080,
},
args: ['--no-sandbox'],
...options,
});
}
interface IMimeType {
[key: string]: string;
}
export const startServer = (defaultPort = 3030) =>
new Promise<http.Server>((resolve) => {
const mimeType: IMimeType = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
};
const s = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url!);
const sanitizePath = path
.normalize(parsedUrl.pathname!)
.replace(/^(\.\.[\/\\])+/, '');
let pathname = path.join(__dirname, sanitizePath);
if (/^\/rrweb.*\.c?js.*/.test(sanitizePath)) {
pathname = path.join(__dirname, `../dist`, sanitizePath);
}
try {
const data = fs.readFileSync(pathname);
const ext = path.parse(pathname).ext;
res.setHeader('Content-type', mimeType[ext] || 'text/plain');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader('Access-Control-Allow-Headers', 'Content-type');
setTimeout(() => {
res.end(data);
// mock delay
}, 100);
} catch (error) {
res.end();
}
});
s.listen(defaultPort)
.on('listening', () => {
resolve(s);
})
.on('error', (e) => {
s.listen().on('listening', () => {
resolve(s);
});
});
});
export function getServerURL(server: http.Server): string {
const address = server.address();
if (address && typeof address !== 'string') {
return `http://localhost:${address.port}`;
} else {
return `${address}`;
}
}
/**
* Puppeteer may cast random mouse move which make our tests flaky.
* So we only do snapshot test with filtered events.
* Also remove timestamp from event.
* @param snapshots incrementalSnapshotEvent[]
*/
function stringifySnapshots(snapshots: eventWithTime[]): string {
return JSON.stringify(
snapshots
.filter((s) => {
if (
s.type === EventType.IncrementalSnapshot &&
(s.data.source === IncrementalSource.MouseMove ||
s.data.source === IncrementalSource.ViewportResize)
) {
return false;
}
return true;
})
.map((s) => {
if (s.type === EventType.Meta) {
s.data.href = 'about:blank';
}
// FIXME: travis coordinates seems different with my laptop
const coordinatesReg =
/(bottom|top|left|right|width|height): \d+(\.\d+)?px/g;
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.MouseInteraction
) {
delete (s.data as Optional<mouseInteractionData, 'x'>).x;
delete (s.data as Optional<mouseInteractionData, 'y'>).y;
}
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.Mutation
) {
s.data.attributes.forEach((a) => {
if ('style' in a.attributes && a.attributes.style) {
if (typeof a.attributes.style === 'object') {
for (const [k, v] of Object.entries(a.attributes.style)) {
if (Array.isArray(v)) {
if (coordinatesReg.test(k + ': ' + v[0])) {
// TODO: could round the number here instead depending on what's coming out of various test envs
a.attributes.style[k] = ['Npx', v[1]];
}
} else if (typeof v === 'string') {
if (coordinatesReg.test(k + ': ' + v)) {
a.attributes.style[k] = 'Npx';
}
}
coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
}
} else if (coordinatesReg.test(a.attributes.style)) {
a.attributes.style = a.attributes.style.replace(
coordinatesReg,
'$1: Npx',
);
}
}
// strip blob:urls as they are different every time
stripBlobURLsFromAttributes(a);
});
s.data.adds.forEach((add) => {
if (add.node.type === NodeType.Element) {
if (
'style' in add.node.attributes &&
typeof add.node.attributes.style === 'string' &&
coordinatesReg.test(add.node.attributes.style)
) {
add.node.attributes.style = add.node.attributes.style.replace(
coordinatesReg,
'$1: Npx',
);
}
coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
// strip blob:urls as they are different every time
stripBlobURLsFromAttributes(add.node);
// strip rr_dataURL as they are not consistent
if (
'rr_dataURL' in add.node.attributes &&
add.node.attributes.rr_dataURL &&
typeof add.node.attributes.rr_dataURL === 'string'
) {
add.node.attributes.rr_dataURL =
add.node.attributes.rr_dataURL.replace(/,.+$/, ',...');
}
}
});
} else if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.MediaInteraction
) {
// round the currentTime to 1 decimal place
if (s.data.currentTime) {
s.data.currentTime = Math.round(s.data.currentTime * 10) / 10;
}
} else if (
s.type === EventType.Plugin &&
s.data.plugin === 'rrweb/console@1'
) {
const pluginPayload = (
s as pluginEvent<{
trace: string[];
payload: string[];
}>
).data.payload;
if (pluginPayload?.trace.length) {
pluginPayload.trace = pluginPayload.trace.map((trace) => {
return trace.replace(
/^pptr:evaluate;.*?:(\d+:\d+)/,
'__puppeteer_evaluation_script__:$1',
);
});
}
if (pluginPayload?.payload.length) {
pluginPayload.payload = pluginPayload.payload.map((payload) => {
return payload.replace(
/pptr:evaluate;.*?:(\d+:\d+)/g,
'__puppeteer_evaluation_script__:$1',
);
});
}
}
delete (s as Optional<eventWithTime, 'timestamp'>).timestamp;
return s as event;
}),
null,
2,
).replace(
// servers might get run on a random port,
// so we need to normalize the port number
/http:\/\/localhost:\d+/g,
'http://localhost:3030',
);
}
function stripBlobURLsFromAttributes(node: {
attributes: {
src?: string;
};
}) {
if (
'src' in node.attributes &&
node.attributes.src &&
typeof node.attributes.src === 'string' &&
node.attributes.src.startsWith('blob:')
) {
node.attributes.src = node.attributes.src
.replace(/[\w-]+$/, '...')
.replace(/:[0-9]+\//, ':xxxx/');
}
}
export async function assertSnapshot(
snapshotsOrPage: eventWithTime[] | puppeteer.Page,
) {
let snapshots: eventWithTime[];
if (!Array.isArray(snapshotsOrPage)) {
// make sure page has finished executing js
await waitForRAF(snapshotsOrPage);
await snapshotsOrPage.waitForFunction(
'window.snapshots && window.snapshots.length > 0',
);
snapshots = (await snapshotsOrPage.evaluate(
'window.snapshots',
)) as eventWithTime[];
} else {
snapshots = snapshotsOrPage;
}
expect(snapshots).toBeDefined();
expect(stringifySnapshots(snapshots)).toMatchSnapshot();
}
export async function waitForRAF(
pageOrFrame: puppeteer.Page | puppeteer.Frame,
) {
return await pageOrFrame.evaluate(() => {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
});
});
}
export async function waitForIFrameLoad(
page: puppeteer.Frame | puppeteer.Page,
iframeSelector: string,
timeout = 10000,
): Promise<puppeteer.Frame> {
const el = await page.waitForSelector(iframeSelector);
if (!el)
throw new Error('Waiting for iframe load has timed out - no element found');
let frame = await el.contentFrame();
if (frame && frame.isDetached()) {
throw new Error(
'Waiting for iframe load has timed out - frame is detached',
);
}
if (frame && frame.url() !== '') {
return frame;
}
await page.$eval(
iframeSelector,
async (iframe, timeout) => {
await new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(
() => reject(new Error('Waiting for iframe load has timed out')),
timeout,
);
iframe.addEventListener('load', () => {
clearTimeout(timeoutId);
resolve();
});
});
},
timeout,
);
frame = await el.contentFrame();
if (!frame)
throw new Error('Waiting for iframe load has timed out - no frame');
return frame;
}