Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { TimersLike } from "@suaveplan/types";
import { AnalyticsTracker } from "../src/tracker/tracker.js";
import { BatchTransport } from "../src/transport/batch.js";
const noopTimers: TimersLike = {
setTimer: () => 0,
clearTimer: () => {},
};
function* walk(dir: string): Generator<string> {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) {
yield* walk(full);
} else if (full.endsWith(".ts") && !full.endsWith(".test.ts")) {
yield full;
}
}
}
describe("integration", () => {
it("tracker → batch transport → in-process endpoint round-trip", {
timeout: 5000,
}, async () => {
const observed: Array<{ events: unknown[] }> = [];
const fetchFn: typeof fetch = async (_url, init) => {
const body = init?.body as string;
observed.push(JSON.parse(body) as { events: unknown[] });
return new Response("", { status: 200 });
};
const transport = new BatchTransport({
endpoint: "/api/analytics",
fetchFn,
});
const tracker = new AnalyticsTracker(transport, {
batchSize: 1,
timers: noopTimers,
});
const seen: string[] = [];
tracker.events.on("track", () => seen.push("track"));
tracker.events.on("flush", ({ success }) =>
seen.push(`flush:${String(success)}`)
);
tracker.track("e");
await tracker.flush();
expect(observed.length).toBe(1);
expect((observed[0]?.events?.[0] as { name: string }).name).toBe("e");
expect(seen[0]).toBe("track");
expect(seen[seen.length - 1]).toBe("flush:true");
});
it("src/ contains no node: imports", { timeout: 5000 }, () => {
const srcDir = join(__dirname, "..", "src");
for (const file of walk(srcDir)) {
const text = readFileSync(file, "utf8");
expect(text).not.toMatch(/from\s+["']node:/);
}
});
});

View File

@@ -0,0 +1,41 @@
/**
* Test setup for @suaveplan/analytics-browser.
*
* Installs a real in-memory OpenTelemetry MeterProvider BEFORE any source
* module imports so the meter resolved on first call binds to it. Also
* polyfills `globalThis.crypto` for happy-dom test environments where the
* subtle crypto provider may be missing.
*/
import { webcrypto } from "node:crypto";
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
type WritableGlobal = { crypto?: Crypto };
const g = globalThis as WritableGlobal;
if (g.crypto === undefined || g.crypto.subtle === undefined) {
g.crypto = webcrypto as unknown as Crypto;
}
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter: testInMemoryMetricExporter,
exportIntervalMillis: 60_000,
});
export const testMeterProvider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(testMeterProvider);
export async function flushTestMetrics(): Promise<unknown> {
await testMeterProvider.forceFlush();
const collected = testInMemoryMetricExporter.getMetrics();
testInMemoryMetricExporter.reset();
return collected;
}