Skip to content

Commit

Permalink
fix(insights): minify ping script and externalize zod
Browse files Browse the repository at this point in the history
zod was being built into the library, and the script wasn't minified and was actually broken because fn.toString() didn't include a helper that rollup injected

- used sync$ to safely provide a function to call in the browser
- uses qinit event which runs before symbols are loaded
- injected globals as function arguments to minify
- used const strings to metaprogram the calls to minify
- used undefined instead of null to save some bytes
  • Loading branch information
wmertens committed Oct 9, 2024
1 parent 5a320d1 commit 6f24b81
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 125 deletions.
1 change: 1 addition & 0 deletions packages/insights/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ node_modules
.rollup.cache
tsconfig.tsbuildinfo
tmp
q-insights.json

# Logs
logs
Expand Down
3 changes: 2 additions & 1 deletion packages/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"undici": "*",
"vite": "5.3.5",
"vite-tsconfig-paths": "4.3.2",
"vitest": "2.0.5"
"vitest": "2.0.5",
"zod": "3.22.4"
},
"engines": {
"node": ">=16.8.0 <18.0.0 || >=18.11"
Expand Down
2 changes: 1 addition & 1 deletion packages/insights/src/db/query-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function createEdgeRow({
}: {
publicApiKey: string;
manifestHash: string;
from: string | null;
from?: string | null;
to: string;
interaction: boolean;
delayBucket: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/insights/src/db/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export async function updateEdge(
edge: {
publicApiKey: string;
manifestHash: string;
from: string | null;
from?: string | null;
to: string;
interaction: boolean;
delayBucket: number;
Expand All @@ -202,7 +202,7 @@ export async function updateEdge(
and(
eq(edgeTable.manifestHash, edge.manifestHash),
eq(edgeTable.publicApiKey, edge.publicApiKey),
edge.from === null ? isNull(edgeTable.from) : eq(edgeTable.from, edge.from),
edge.from == null ? isNull(edgeTable.from) : eq(edgeTable.from, edge.from),
eq(edgeTable.to, edge.to)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export const onPost: RequestHandler = async ({ exit, json, request, params }) =>
}
};

function cleanupSymbolName(symbolName: string | null): string | null {
if (!symbolName) return null;
function cleanupSymbolName(symbolName?: string | null) {
if (!symbolName) return;
const shortName = symbolName.substring(symbolName.lastIndexOf('_') + 1 || 0);
if (shortName == 'hW') return null;
if (shortName == 'hW') return;
return shortName;
}
function migrate1(payloadJson: any) {
Expand Down
6 changes: 5 additions & 1 deletion packages/qwik-labs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"prettier": "3.3.3",
"typescript": "5.4.5",
"undici": "*",
"vite": "5.3.5"
"vite": "5.3.5",
"zod": "3.22.4"
},
"peerDependencies": {
"zod": "3.22.4"
},
"engines": {
"node": ">=16.8.0 <18.0.0 || >=18.11"
Expand Down
261 changes: 144 additions & 117 deletions packages/qwik-labs/src/insights/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { component$ } from '@builder.io/qwik';
import { component$, sync$ } from '@builder.io/qwik';
import { z } from 'zod';

export interface InsightsPayload {
Expand All @@ -22,7 +22,7 @@ export interface InsightsPayload {
* is useful for server clustering. Sending previous symbol name allows the server to stitch the
* symbol list together.
*/
previousSymbol: string | null;
previousSymbol?: string | null;

/** List of symbols which have been received since last update. */
symbols: InsightSymbol[];
Expand Down Expand Up @@ -64,7 +64,7 @@ export interface InsightsError {
stack: string;
}

export const InsightsError = z.object({
export const InsightsError = /* @__PURE__ */ z.object({
manifestHash: z.string(),
url: z.string(),
timestamp: z.number(),
Expand All @@ -76,7 +76,7 @@ export const InsightsError = z.object({
stack: z.string(),
});

export const InsightSymbol = z.object({
export const InsightSymbol = /* @__PURE__ */ z.object({
symbol: z.string(),
route: z.string(),
delay: z.number(),
Expand All @@ -85,35 +85,19 @@ export const InsightSymbol = z.object({
interaction: z.boolean(),
});

export const InsightsPayload = z.object({
export const InsightsPayload = /* @__PURE__ */ z.object({
qVersion: z.string(),
manifestHash: z.string(),
publicApiKey: z.string(),
previousSymbol: z.string().nullable(),
// we retain nullable for older clients
previousSymbol: z.string().optional().nullable(),
symbols: z.array(InsightSymbol),
});

InsightSymbol._type satisfies InsightSymbol;
InsightsPayload._type satisfies InsightsPayload;
InsightsError._type satisfies InsightsError;

export const Insights = component$<{ publicApiKey: string; postUrl?: string }>(
({ publicApiKey, postUrl }) => {
return (
<script
data-insights={publicApiKey}
dangerouslySetInnerHTML={`(${symbolTracker.toString()})(window, document, location, navigator, ${JSON.stringify(
publicApiKey
)},
${JSON.stringify(
postUrl || 'https://qwik-insights.builder.io/api/v1/${publicApiKey}/post/'
)}
)`}
/>
);
}
);

interface QwikSymbolTrackerWindow extends Window {
qSymbolTracker: {
symbols: InsightSymbol[];
Expand All @@ -122,105 +106,148 @@ interface QwikSymbolTrackerWindow extends Window {
}

interface QSymbolDetail {
element: HTMLElement | null;
element: HTMLElement | undefined;
reqTime: number;
symbol: string;
}

function symbolTracker(
window: QwikSymbolTrackerWindow,
document: Document,
location: Location,
navigator: Navigator,
publicApiKey: string,
postUrl: string
) {
const qVersion = document.querySelector('[q\\:version]')?.getAttribute('q:version') || 'unknown';
const manifestHash =
document.querySelector('[q\\:manifest-hash]')?.getAttribute('q:manifest-hash') || 'dev';
const qSymbols: InsightSymbol[] = [];
const existingSymbols: Set<string> = new Set();
let flushSymbolIndex: number = 0;
let lastReqTime: number = 0;
window.qSymbolTracker = {
symbols: qSymbols,
publicApiKey,
};
let timeoutID: ReturnType<typeof setTimeout> | null;
let qRouteChangeTime = performance.now();
const qRouteEl = document.querySelector('[q\\:route]');
if (qRouteEl) {
const observer = new MutationObserver((mutations) => {
const mutation = mutations.find((m) => m.attributeName === 'q:route');
if (mutation) {
qRouteChangeTime = performance.now();
// We use a self-invoking function to minify the code, renaming long globals and attributes
// the qwik optimizer only minifies somewhat, so put all var declarations in the same line
const insightsPing = sync$(() =>
((
window: QwikSymbolTrackerWindow,
document,
location,
navigator,
performance,
round,
JSON_stringify
) => {
/* eslint-disable no-var -- better minification */
var publicApiKey = __QI_KEY__,
postUrl = __QI_URL__,
getAttribute_s = 'getAttribute' as const,
querySelector_s = 'querySelector' as const,
manifest_s = 'manifest' as const,
manifest_hash_s = `${manifest_s}-hash` as const,
manifestHash_s = `${manifest_s}Hash` as const,
version_s = 'version' as const,
publicApiKey_s = 'publicApiKey' as const,
sendBeacon_s = 'sendBeacon' as const,
symbol_s = 'symbol' as const,
length_s = 'length' as const,
addEventListener_s = 'addEventListener' as const,
route_s = 'route' as const,
error_s = 'error' as const,
stack_s = 'stack' as const,
message_s = 'message' as const,
symbols_s = `${symbol_s}s` as const,
qVersion =
document[querySelector_s](`[q\\:${version_s}]`)?.[getAttribute_s](`q:${version_s}`) ||
'unknown',
manifestHash =
document[querySelector_s](`[q\\:${manifest_hash_s}]`)?.[getAttribute_s](
`q:${manifest_hash_s}`
) || 'dev',
qSymbols: InsightSymbol[] = [],
existingSymbols: Set<string> = new Set(),
flushSymbolIndex: number = 0,
lastReqTime: number = 0,
timeoutID: ReturnType<typeof setTimeout> | undefined,
qRouteChangeTime = performance.now(),
qRouteEl = document[querySelector_s](`[q\\:${route_s}]`),
flush = () => {
timeoutID = undefined;
if (qSymbols[length_s] > flushSymbolIndex) {
var payload = {
qVersion,
[publicApiKey_s]: publicApiKey,
[manifestHash_s]: manifestHash,
previousSymbol:
flushSymbolIndex == 0 ? undefined : qSymbols[flushSymbolIndex - 1][symbol_s],
[symbols_s]: qSymbols.slice(flushSymbolIndex),
} satisfies InsightsPayload;
navigator[sendBeacon_s](postUrl, JSON_stringify(payload));
flushSymbolIndex = qSymbols[length_s];
}
},
debounceFlush = () => {
timeoutID != undefined && clearTimeout(timeoutID);
timeoutID = setTimeout(flush, 1000);
};

window.qSymbolTracker = {
[symbols_s]: qSymbols,
[publicApiKey_s]: publicApiKey,
};
if (qRouteEl) {
new MutationObserver((mutations) => {
var mutation = mutations.find((m) => m.attributeName === `q:${route_s}`);
if (mutation) {
qRouteChangeTime = performance.now();
}
}).observe(qRouteEl, { attributes: true });
}
document[addEventListener_s](
'visibilitychange',
() => document.visibilityState === 'hidden' && flush()
);
document[addEventListener_s](`q${symbol_s}`, (_event) => {
var event = _event as CustomEvent<QSymbolDetail>,
detail = event.detail,
symbolRequestTime = detail.reqTime,
symbolDeliveredTime = event.timeStamp,
symbol = detail[symbol_s];
if (!existingSymbols.has(symbol)) {
existingSymbols.add(symbol);
var route = qRouteEl?.[getAttribute_s](`q:${route_s}`) || '/';
qSymbols.push({
[symbol_s]: symbol,
[route_s]: route,
delay: round(0 - lastReqTime + symbolRequestTime),
latency: round(symbolDeliveredTime - symbolRequestTime),
timeline: round(0 - qRouteChangeTime + symbolRequestTime),
interaction: !!detail.element,
});
lastReqTime = symbolDeliveredTime;
debounceFlush();
}
});
observer.observe(qRouteEl, { attributes: true });
}
function flush() {
timeoutID = null;
if (qSymbols.length > flushSymbolIndex) {
const payload = {
qVersion,
publicApiKey,
manifestHash,
previousSymbol: flushSymbolIndex == 0 ? null : qSymbols[flushSymbolIndex - 1].symbol,
symbols: qSymbols.slice(flushSymbolIndex),
} satisfies InsightsPayload;
navigator.sendBeacon(
postUrl.replace('${publicApiKey}', publicApiKey),
JSON.stringify(payload)
);
flushSymbolIndex = qSymbols.length;
}
}
function debounceFlush() {
timeoutID != null && clearTimeout(timeoutID);
timeoutID = setTimeout(flush, 1000);
}
document.addEventListener(
'visibilitychange',
() => document.visibilityState === 'hidden' && flush()
);
document.addEventListener('qsymbol', (_event) => {
const event = _event as CustomEvent<QSymbolDetail>;
const detail = event.detail;
const symbolRequestTime = detail.reqTime;
const symbolDeliveredTime = event.timeStamp;
const symbol = detail.symbol;
if (!existingSymbols.has(symbol)) {
existingSymbols.add(symbol);
const route = qRouteEl?.getAttribute('q:route') || '/';
qSymbols.push({
symbol: symbol,
route,
delay: Math.round(0 - lastReqTime + symbolRequestTime),
latency: Math.round(symbolDeliveredTime - symbolRequestTime),
timeline: Math.round(0 - qRouteChangeTime + symbolRequestTime),
interaction: !!detail.element,
});
lastReqTime = symbolDeliveredTime;
debounceFlush();
window[addEventListener_s](error_s, (event: ErrorEvent) => {
var error = event[error_s];
if (!(error && typeof error === 'object')) return;
var payload = {
url: `${location}`,
[manifestHash_s]: manifestHash,
timestamp: new Date().getTime(),
source: event.filename,
line: event.lineno,
column: event.colno,
[message_s]: event[message_s],
[error_s]: message_s in error ? (error as Error)[message_s] : `${error}`,
[stack_s]: stack_s in error ? (error as Error)[stack_s] || '' : '',
} satisfies InsightsError;
navigator[sendBeacon_s](`${postUrl}${error_s}/`, JSON_stringify(payload));
});
})(window as any, document, location, navigator, performance, Math.round, JSON.stringify)
);

// We don't add window. to save some bytes
declare var __QI_KEY__: string;
declare var __QI_URL__: string;

export const Insights = component$<{ publicApiKey: string; postUrl?: string }>(
({ publicApiKey, postUrl }) => {
if (!publicApiKey) {
return null;
}
});
window.addEventListener('error', (event: ErrorEvent) => {
const error = event.error;
if (!(error && typeof error === 'object')) return;
const payload = {
url: location.toString(),
manifestHash,
timestamp: new Date().getTime(),
source: event.filename,
line: event.lineno,
column: event.colno,
message: event.message,
error: 'message' in error ? (error as Error).message : String(error),
stack: 'stack' in error ? (error as Error).stack || '' : '',
} satisfies InsightsError;
navigator.sendBeacon(
postUrl.replace('${publicApiKey}', publicApiKey) + 'error/',
JSON.stringify(payload)

return (
// the script will set the variables before the qinit event
<script
document:onQInit$={insightsPing}
dangerouslySetInnerHTML={`__QI_KEY__=${JSON.stringify(publicApiKey)};__QI_URL__=${JSON.stringify(postUrl || `https://insights.qwik.dev/api/v1/${publicApiKey}/post/`)}`}
/>
);
});
}
}
);
3 changes: 3 additions & 0 deletions packages/qwik-labs/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export default defineConfig(() => {
formats: ['es', 'cjs'],
fileName: (format) => `index.qwik.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
external: ['zod'],
},
},
plugins: [qwikVite(), dtsPlugin()],
};
Expand Down
Loading

0 comments on commit 6f24b81

Please sign in to comment.