import assert from "node:assert/strict"; import { createCipheriv, randomBytes } from "node:crypto"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { createServer } from "node:http"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { build } from "esbuild"; import { chromium } from "@playwright/test"; // Exercise the shipped SDK and React provider. Every report terminates in a // browser route; synthetic configuration never reaches an analytics service. const root = process.cwd(); const temp = await mkdtemp(join(tmpdir(), "huakui-tracker-")); const sdk = await readFile(join(root, "public/static/common/web-sdk-v1.2.0.js"), "utf8"); const configKey = sdk.match(/_secondGcmKeyBase64="([^"\n]+)"/)?.[1]; assert.ok(configKey, "The SDK configuration encryption contract is available"); const iv = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", Buffer.from(configKey, "base64"), iv); const encryptedConfig = Buffer.concat([ iv, cipher.update(JSON.stringify({ batchUrls: ["https://analytics.fixture.invalid"], eventCodes: ["app_page_view", "ad_impression", "page_click", "ad_click"], signKey: "synthetic-tracker-fixture-key", securityKeyVersion: "fixture", })), cipher.final(), cipher.getAuthTag(), ]).toString("base64"); const appFile = join(temp, "app.js"); let browser: Awaited> | undefined; let server: ReturnType | undefined; try { await build({ stdin: { contents: `import {createRoot} from 'react-dom/client'; import {TrackerProvider} from './src/components/analytics/TrackerProvider'; createRoot(document.getElementById('root')).render( 广告验证 );`, loader: "tsx", resolveDir: root, }, outfile: appFile, bundle: true, platform: "browser", format: "iife", tsconfig: join(root, "tsconfig.json"), define: { "process.env.NODE_ENV": '"production"' }, }); const app = await readFile(appFile); server = createServer((request, response) => { const path = new URL(request.url || "/", "http://fixture").pathname; if (path === "/app.js") { response.setHeader("Content-Type", "application/javascript").end(app); } else if (path === "/static/common/web-sdk-v1.2.0.js") { response.setHeader("Content-Type", "application/javascript").end(sdk); } else if (path === "/") { response.setHeader("Content-Type", "text/html; charset=utf-8").end( 'Tracker fixture
', ); } else response.writeHead(404).end(); }); await new Promise((done) => server!.listen(0, "127.0.0.1", done)); const address = server.address(); assert.ok(address && typeof address !== "string"); const origin = `http://127.0.0.1:${address.port}`; browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", headless: true, }); const commonKeys = ["sid", "device_id", "device", "device_fingerprint", "fp_version"].sort(); const results: Record[] = []; for (const fixture of [ { device: "iOS", system: "iOS", version: "18.0", ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1", }, { device: "Android", system: "Android", version: "14", ua: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/130.0.0.0 Mobile Safari/537.36", }, { device: "PC", system: "Windows", version: "10.0", ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130.0.0.0 Safari/537.36", }, { device: "PC", system: "macOS", version: "14.6", ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15", }, ]) { const context = await browser.newContext({ userAgent: fixture.ua }); const reports: Record[] = []; const errors: string[] = []; await context.route("**/*", async (route) => { const request = route.request(); const url = new URL(request.url()); if (url.origin === origin) return route.continue(); if (url.hostname === "analytics.fixture.invalid") { if (request.method() === "POST") { const body: unknown = JSON.parse(request.postData() || "[]"); assert.ok(Array.isArray(body), "SDK reports use a top-level batch array"); for (const record of body) { assert.ok(record && typeof record === "object"); reports.push(record as Record); } } return route.fulfill({ status: 200, contentType: "application/json", body: "{}" }); } return route.abort(); }); const page = await context.newPage(); page.on("pageerror", (error) => errors.push(error.message)); await page.goto(origin, { waitUntil: "load" }); await page.waitForFunction(() => !!window.__trackerCommonFields, null, { timeout: 15000 }); await page.waitForFunction(() => !!window.tracker, null, { timeout: 15000 }); const common = await page.evaluate(async () => { const fields = await window.WebSDK!.getCommonFields!(["device", "system_name", "system_version"]); const header = await window.tracker!.getSDKCommonHeader(); return { fields, headerKeys: Object.keys(JSON.parse(atob(header))).sort() }; }); assert.equal(common.fields.device, fixture.device); assert.equal(common.fields.system_name, fixture.system); assert.equal(common.fields.system_version, fixture.version); assert.deepEqual(common.headerKeys, commonKeys, "X-SDK-Common retains its five-field contract"); await page.waitForFunction(async () => { await window.tracker!.flush(); return true; }); await assertEventually(() => reports.some((r) => r.event === "app_page_view") && reports.some((r) => r.event === "ad_impression")); for (const event of ["app_page_view", "ad_impression"]) { const report = reports.find((record) => record.event === event); assert.ok(report, `${fixture.system}: ${event} was sent through the real SDK`); assert.equal(report.device, fixture.device); assert.equal(report.system_name, fixture.system); assert.equal(report.system_version, fixture.version); assert.equal(report.app_id, "JHA-4001"); assert.ok(typeof report.event_sig === "string" && report.event_sig.length > 0, "Real SDK signs the report"); } assert.deepEqual(errors, []); results.push({ system: fixture.system, version: fixture.version, events: ["app_page_view", "ad_impression"], headerKeys: common.headerKeys.length }); await context.close(); } console.log(JSON.stringify({ passed: true, cases: results }, null, 2)); } finally { await browser?.close(); if (server) await new Promise((done) => server!.close(() => done())); await rm(temp, { recursive: true, force: true }); } async function assertEventually(predicate: () => boolean) { const deadline = Date.now() + 15000; while (!predicate() && Date.now() < deadline) await new Promise((done) => setTimeout(done, 50)); assert.ok(predicate(), "The SDK sent both the page view and visible ad impression"); }