Files
stats/apps/api/scripts/mock.ts

514 lines
16 KiB
TypeScript

import fs from 'node:fs';
import * as faker from '@faker-js/faker';
import { generateId } from '@openpanel/common';
import { hashPassword } from '@openpanel/common/server';
import { ClientType, db } from '@openpanel/db';
import { getRedisCache } from '@openpanel/redis';
import { v4 as uuidv4 } from 'uuid';
const DOMAIN_COUNT = 5;
const PROFILE_COUNT = 50;
interface Event {
track: Track;
headers: Record<string, string>;
}
interface Track {
type: 'track';
payload: {
name: string;
properties: Record<string, string>;
};
}
interface Profile {
id?: string;
name?: string;
userAgent: string;
ip: string;
}
const domains = Array.from({ length: DOMAIN_COUNT }, () => ({
domain: `https://${faker.allFakers.en.internet.domainName()}`,
clientId: uuidv4(),
profiles: Array.from({ length: PROFILE_COUNT }, () => ({
// id: uuidv4(),
// name: faker.allFakers.en.name.findName(),
userAgent: faker.allFakers.en.internet.userAgent(),
ip: faker.allFakers.en.internet.ipv4(),
})),
}));
const referrers = [
'',
'https://www.google.com',
'https://www.facebook.com',
'https://www.twitter.com',
'https://www.linkedin.com',
'https://www.bing.com',
'https://www.duckduckgo.com',
'https://www.baidu.com',
'https://www.yandex.com',
'https://www.pinterest.com',
'https://www.reddit.com',
'https://www.tumblr.com',
'https://www.flickr.com',
'https://www.vimeo.com',
'https://www.mixcloud.com',
'',
];
function generatePath(): string {
const basePath = `/${faker.allFakers.en.lorem.slug()}`;
const queryString =
Math.random() < 0.7
? `?${faker.allFakers.en.internet.domainWord()}=${faker.allFakers.en.lorem.word()}`
: '';
const hashFragment =
Math.random() < 0.3 ? `#${faker.allFakers.en.lorem.word()}` : '';
return `${basePath}${queryString}${hashFragment}`;
}
async function trackit(event: Event) {
console.log('trackit', JSON.stringify(event.track, null, 2));
const res = await fetch('http://localhost:3333/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...event.headers,
},
body: JSON.stringify(event.track),
});
if (res.ok) {
return true;
}
console.error('Failed to track event', res.status, res.statusText);
return false;
}
function generateScreenViews({
domain,
clientId,
profile,
eventsCount,
}: {
domain: string;
clientId: string;
profile: Profile;
eventsCount: number;
}): Event[] {
return Array.from({ length: eventsCount }, (_, index) => ({
headers: {
'openpanel-client-id': clientId,
'x-client-ip': profile.ip,
'user-agent': profile.userAgent,
origin: domain,
},
track: {
type: 'track',
payload: {
name: 'screen_view',
properties: {
__referrer:
referrers[Math.floor(Math.random() * referrers.length)] ?? '',
__path: `${domain}${generatePath()}`,
__title: faker.allFakers.en.lorem.sentence(),
},
},
},
}));
}
function generateEvents(): Event[] {
const events: Event[] = [];
domains.forEach(({ domain, clientId, profiles }) => {
for (let i = 0; i < profiles.length; i++) {
events.push(
...generateScreenViews({
domain,
clientId,
profile: profiles[i % PROFILE_COUNT]!,
eventsCount: Math.floor(Math.random() * 10),
})
);
}
});
return events;
}
function scrambleEvents(events: Event[]) {
return events.sort(() => Math.random() - 0.5);
}
let lastTriggeredIndex = 0;
async function triggerEvents(generatedEvents: any[]) {
const EVENTS_PER_SECOND = Number.parseInt(
process.env.EVENTS_PER_SECOND || '100',
10
);
const INTERVAL_MS = 1000 / EVENTS_PER_SECOND;
if (lastTriggeredIndex >= generatedEvents.length) {
console.log('All events triggered.');
return;
}
const event = generatedEvents[lastTriggeredIndex]!;
try {
await trackit(event);
console.log(`Event ${lastTriggeredIndex + 1} sent successfully`);
console.log(
`sending ${event.track.payload?.properties?.__path} from user ${event.headers['user-agent']}`
);
} catch (error) {
console.error(`Failed to send event ${lastTriggeredIndex + 1}:`, error);
}
lastTriggeredIndex++;
const remainingEvents = generatedEvents.length - lastTriggeredIndex;
console.log(
`Triggered ${lastTriggeredIndex} events. Remaining: ${remainingEvents}`
);
if (remainingEvents > 0) {
return new Promise((resolve) => {
setTimeout(() => {
triggerEvents(generatedEvents);
resolve(null);
}, INTERVAL_MS);
});
}
console.log('All events triggered.');
console.log(`Total events to trigger: ${generatedEvents.length}`);
}
async function createMock(file: string) {
for (const project of domains) {
await db.project.create({
data: {
organizationId: 'openpanel-dev',
name: project.domain,
cors: [project.domain],
domain: project.domain,
crossDomain: true,
clients: {
create: {
organizationId: 'openpanel-dev',
name: project.domain,
secret: await hashPassword('secret'),
id: project.clientId,
type: ClientType.write,
},
},
},
});
}
fs.writeFileSync(
file,
JSON.stringify(insertFakeEvents(scrambleEvents(generateEvents())), null, 2),
'utf-8'
);
}
function insertFakeEvents(events: Event[]) {
const blueprint = {
headers: {
'openpanel-client-id': '5b679c47-9ec0-470a-8944-a9ab8f42b14f',
'x-client-ip': '229.145.77.175',
'user-agent':
'Opera/13.66 (Macintosh; Intel Mac OS X 10.8.3 U; GV Presto/2.9.183 Version/11.00)',
origin: 'https://classic-hovel.info',
},
track: {
type: 'track',
payload: {
name: 'screen_view',
properties: {
__referrer: 'https://www.google.com',
__path: 'https://classic-hovel.info/beneficium-arcesso-quisquam',
__title: 'Hic thesis laboriosam copiose admoveo sufficio.',
},
},
},
};
const newEvents = [];
for (const event of events) {
(event.track.payload.properties as any).__group = generateId();
newEvents.push(event);
if (event.track.payload.name === 'screen_view' && Math.random() < 0.5) {
const fakeEvent = JSON.parse(JSON.stringify(blueprint));
fakeEvent.track.payload.name = faker.allFakers.en.lorem.word();
fakeEvent.headers = event.headers;
delete fakeEvent.track.payload.properties;
newEvents.push(fakeEvent);
fakeEvent.track.payload.properties = {
__group: (event.track.payload.properties as any).__group,
};
}
}
return newEvents;
}
async function simultaneousRequests() {
await getRedisCache().flushdb();
await new Promise((resolve) => setTimeout(resolve, 1000));
const sessions: {
ip: string;
referrer: string;
userAgent: string;
track: Record<string, string>[];
}[] = [
{
ip: '122.168.1.101',
referrer: 'https://www.google.com',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
track: [
{ name: 'screen_view', path: '/home', parallel: '1' },
{ name: 'button_click', element: 'signup', parallel: '1' },
{ name: 'article_viewed', articleId: '123', parallel: '1' },
{ name: 'screen_view', path: '/pricing', parallel: '1' },
{ name: 'screen_view', path: '/blog', parallel: '1' },
],
},
{
ip: '192.168.1.101',
referrer: 'https://www.bing.com',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
track: [{ name: 'screen_view', path: '/landing' }],
},
{
ip: '192.168.1.102',
referrer: 'https://www.google.com',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
track: [{ name: 'screen_view', path: '/about' }],
},
{
ip: '192.168.1.103',
referrer: '',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1',
track: [
{ name: 'screen_view', path: '/home' },
{ name: 'form_submit', form: 'contact' },
],
},
{
ip: '192.168.1.104',
referrer: '',
userAgent:
'Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
track: [{ name: 'screen_view', path: '/products' }],
},
{
ip: '203.0.113.101',
referrer: 'https://www.facebook.com',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0',
track: [
{ name: 'video_play', videoId: 'abc123' },
{ name: 'button_click', element: 'subscribe' },
],
},
{
ip: '203.0.113.55',
referrer: 'https://www.twitter.com',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1',
track: [
{ name: 'screen_view', path: '/blog' },
{ name: 'scroll', depth: '50%' },
],
},
{
ip: '198.51.100.20',
referrer: 'https://www.linkedin.com',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.902.62 Safari/537.36 Edg/92.0.902.62',
track: [{ name: 'button_click', element: 'download' }],
},
{
ip: '198.51.100.21',
referrer: 'https://www.google.com',
userAgent:
'Mozilla/5.0 (Linux; Android 10; SM-A505FN) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
track: [
{ name: 'screen_view', path: '/services' },
{ name: 'button_click', element: 'learn_more' },
],
},
{
ip: '203.0.113.60',
referrer: '',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15A5341f Safari/604.1',
track: [{ name: 'form_submit', form: 'feedback' }],
},
{
ip: '208.22.132.143',
referrer: '',
userAgent:
'Mozilla/5.0 (Linux; arm_64; Android 10; MAR-LX1H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 YaBrowser/20.4.4.24.00 (alpha) SA/0 Mobile Safari/537.36',
track: [
{ name: 'screen_view', path: '/landing' },
{ name: 'screen_view', path: '/pricing' },
{ name: 'screen_view', path: '/blog' },
{ name: 'screen_view', path: '/blog/post-1', parallel: '1' },
{ name: 'screen_view', path: '/blog/post-2', parallel: '1' },
{ name: 'button_click', element: 'learn_more', parallel: '1' },
{ name: 'screen_view', path: '/blog/post-3' },
{ name: 'screen_view', path: '/blog/post-4' },
],
},
{
ip: '34.187.95.236',
referrer: 'https://chatgpt.com',
userAgent:
'Mozilla/5.0 (Linux; U; Android 9; ar-eg; Redmi 7 Build/PKQ1.181021.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.141 Mobile Safari/537.36 XiaoMi/MiuiBrowser/12.8.3-gn',
track: [
{ name: 'screen_view', path: '/blog' },
{ name: 'screen_view', path: '/blog/post-1' },
],
},
];
const screenView: Event = {
headers: {
'openpanel-client-id': 'ef38d50e-7d8e-4041-9c62-46d4c3b3bb01',
'x-client-ip': '',
'user-agent': '',
origin: 'https://openpanel.dev',
},
track: {
type: 'track',
payload: {
name: 'screen_view',
properties: {},
},
},
};
for (const session of sessions) {
// Group tracks by parallel flag
const trackGroups: { parallel?: string; tracks: any[] }[] = [];
let currentGroup: { parallel?: string; tracks: any[] } = { tracks: [] };
for (const track of session.track) {
if (track.parallel) {
// If this track has a parallel flag
if (currentGroup.parallel === track.parallel) {
// Same parallel group, add to current group
currentGroup.tracks.push(track);
} else {
// Different parallel group, finish current group and start new one
if (currentGroup.tracks.length > 0) {
trackGroups.push(currentGroup);
}
currentGroup = { parallel: track.parallel, tracks: [track] };
}
} else {
// No parallel flag, finish any parallel group and start individual track
if (currentGroup.tracks.length > 0) {
trackGroups.push(currentGroup);
}
currentGroup = { tracks: [track] };
}
}
// Add the last group
if (currentGroup.tracks.length > 0) {
trackGroups.push(currentGroup);
}
// Process each group
for (const group of trackGroups) {
if (group.parallel && group.tracks.length > 1) {
// Parallel execution for same-flagged tracks
console.log(
`Firing ${group.tracks.length} parallel requests with flag '${group.parallel}'`
);
const promises = group.tracks.map(async (track) => {
const { name, parallel, ...properties } = track;
const event = JSON.parse(JSON.stringify(screenView));
event.track.payload.name = name ?? '';
event.track.payload.properties.__referrer = session.referrer ?? '';
if (name === 'screen_view') {
event.track.payload.properties.__path =
(event.headers.origin ?? '') + (properties.path ?? '');
} else {
event.track.payload.name = track.name ?? '';
event.track.payload.properties = properties;
}
event.headers['x-client-ip'] = session.ip;
event.headers['user-agent'] = session.userAgent;
return trackit(event);
});
await Promise.all(promises);
console.log(`Completed ${group.tracks.length} parallel requests`);
} else {
// Sequential execution for individual tracks
for (const track of group.tracks) {
const { name, parallel, ...properties } = track;
screenView.track.payload.name = name ?? '';
screenView.track.payload.properties.__referrer =
session.referrer ?? '';
if (name === 'screen_view') {
screenView.track.payload.properties.__path =
(screenView.headers.origin ?? '') + (properties.path ?? '');
} else {
screenView.track.payload.name = track.name ?? '';
screenView.track.payload.properties = properties;
}
screenView.headers['x-client-ip'] = session.ip;
screenView.headers['user-agent'] = session.userAgent;
await trackit(screenView);
}
}
// Add delay between groups (not within parallel groups)
// await new Promise((resolve) => setTimeout(resolve, Math.random() * 100));
}
}
}
const exit = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
process.exit(1);
};
async function main() {
const [type, file = 'mock-basic.json'] = process.argv.slice(2);
switch (type) {
case 'send': {
const data = await import(`./${file}`, { assert: { type: 'json' } });
await triggerEvents(data.default);
break;
}
case 'sim':
await simultaneousRequests();
break;
case 'mock':
await createMock(file);
await exit();
break;
default:
console.log('usage: jiti mock.ts send|mock|sim [file]');
}
}
main();