authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 18:44:02-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:40:49-08:00
log14022f1d00a4209762e6bfa1557da9082535b0a5
tree6a0f739ddd4eeb6b1209a551b92b0939a5c71c26
parent28c62f3efde14782e1c0e96c40cc80aabb7282f0

Refactor analytics flush test to use Effect NodeServices

- Wrap test in `it.layer(NodeServices.layer)` for Node service provisioning - Replace manual temp dir setup/cleanup with scoped `FileSystem.makeTempDirectoryScoped` - Keep multi-batch flush assertions while simplifying test runtime wiring

1 files changed, 84 insertions(+), 90 deletions(-)

apps/server/src/telemetry/Layers/AnalyticsService.test.ts+84-90
......@@ -1,10 +1,7 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
51import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
2import * as NodeServices from "@effect/platform-node/NodeServices";
63import { assert, it } from "@effect/vitest";
7import { ConfigProvider, Effect, Layer } from "effect";
4import { ConfigProvider, Effect, FileSystem, Layer } from "effect";
85import * as HttpServer from "effect/unstable/http/HttpServer";
96import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
107import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
......@@ -16,16 +13,14 @@ import { AnalyticsServiceLayerLive } from "./AnalyticsService.ts";
1613
1714interface RecordedBatchRequest {
1815 readonly path: string;
19 readonly body:
20 | {
21 readonly batch?: ReadonlyArray<{
22 readonly event?: string;
23 readonly properties?: {
24 readonly index?: number;
25 };
26 }>;
27 }
28 | null;
16 readonly body: {
17 readonly batch?: ReadonlyArray<{
18 readonly event?: string;
19 readonly properties?: {
20 readonly index?: number;
21 };
22 }>;
23 } | null;
2924}
3025
3126interface RecordedBatchBody {
......@@ -37,85 +32,84 @@ interface RecordedBatchBody {
3732 }>;
3833}
3934
40it.effect("flush drains all buffered events across multiple batches", () =>
41 Effect.gen(function* () {
42 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-telemetry-flush-"));
43 const capturedRequests: Array<RecordedBatchRequest> = [];
44 const telemetryLayer = AnalyticsServiceLayerLive.pipe(
45 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), tempDir)),
46 );
47 const configLayer = ConfigProvider.layer(
48 ConfigProvider.fromUnknown({
49 T3CODE_TELEMETRY_ENABLED: true,
50 T3CODE_POSTHOG_KEY: "phc_test_key",
51 T3CODE_POSTHOG_HOST: "",
52 T3CODE_TELEMETRY_FLUSH_BATCH_SIZE: 20,
53 }),
54 );
55 const batchServerLayer = HttpServer.serve(
56 Effect.gen(function* () {
57 const request = yield* HttpServerRequest.HttpServerRequest;
58 if (request.method !== "POST") {
59 return HttpServerResponse.empty({ status: 404 });
60 }
61
62 const payload = yield* request.json.pipe(
63 Effect.map((body) => body as RecordedBatchRequest["body"]),
64 Effect.catch(() => Effect.succeed(null)),
65 );
66 yield* Effect.sync(() => {
67 capturedRequests.push({ path: request.url, body: payload });
68 });
35it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
36 it.effect("flush drains all buffered events across multiple batches", () =>
37 Effect.gen(function* () {
38 const fileSystem = yield* FileSystem.FileSystem;
39 const stateDir = yield* fileSystem.makeTempDirectoryScoped({
40 prefix: "t3-telemetry-flush-",
41 });
6942
70 return HttpServerResponse.jsonUnsafe({});
71 }),
72 );
73 const runtimeLayer = telemetryLayer.pipe(
74 Layer.provide(configLayer),
75 Layer.provideMerge(NodeHttpServer.layerTest),
76 );
43 const capturedRequests: Array<RecordedBatchRequest> = [];
44 const serverConfigLayer = ServerConfig.layerTest(process.cwd(), stateDir);
7745
78 yield* Effect.gen(function* () {
79 yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped);
80 const telemetryIdentifier = yield* getTelemetryIdentifier;
81 assert.equal(telemetryIdentifier !== null, true);
82 const analytics = yield* AnalyticsService;
46 const telemetryLayer = AnalyticsServiceLayerLive.pipe(Layer.provideMerge(serverConfigLayer));
47 const configLayer = ConfigProvider.layer(
48 ConfigProvider.fromUnknown({
49 T3CODE_TELEMETRY_ENABLED: true,
50 T3CODE_POSTHOG_KEY: "phc_test_key",
51 T3CODE_POSTHOG_HOST: "",
52 T3CODE_TELEMETRY_FLUSH_BATCH_SIZE: 20,
53 }),
54 );
55 const batchServerLayer = HttpServer.serve(
56 Effect.gen(function* () {
57 const request = yield* HttpServerRequest.HttpServerRequest;
58 if (request.method !== "POST") {
59 return HttpServerResponse.empty({ status: 404 });
60 }
8361
84 for (let index = 0; index < 45; index += 1) {
85 yield* analytics.record("test.flush.drain", { index });
86 }
62 const payload = yield* request.json.pipe(
63 Effect.map((body) => body as RecordedBatchRequest["body"]),
64 Effect.catch(() => Effect.succeed(null)),
65 );
66 yield* Effect.sync(() => {
67 capturedRequests.push({ path: request.url, body: payload });
68 });
8769
88 yield* analytics.flush;
89 }).pipe(
90 Effect.provide(runtimeLayer),
91 Effect.ensuring(
92 Effect.sync(() => {
93 fs.rmSync(tempDir, { recursive: true, force: true });
70 return HttpServerResponse.jsonUnsafe({});
9471 }),
95 ),
96 );
72 );
73 const runtimeLayer = telemetryLayer.pipe(
74 Layer.provide(configLayer),
75 Layer.provideMerge(NodeHttpServer.layerTest),
76 );
77
78 yield* Effect.gen(function* () {
79 yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped);
80 const telemetryIdentifier = yield* getTelemetryIdentifier;
81 assert.equal(telemetryIdentifier !== null, true);
82 const analytics = yield* AnalyticsService;
83
84 for (let index = 0; index < 45; index += 1) {
85 yield* analytics.record("test.flush.drain", { index });
86 }
87
88 yield* analytics.flush;
89 }).pipe(Effect.provide(runtimeLayer));
9790
98 const batchRequests = capturedRequests.filter(
99 (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>
100 Array.isArray(request.body?.batch),
101 );
102 assert.equal(batchRequests.length, 3);
103 assert.equal(
104 batchRequests.every((request) => request.path === "/batch/" || request.path === "/batch"),
105 true,
106 );
107 const deliveredIndexes = batchRequests.flatMap((request) =>
108 request.body.batch
109 .filter((event) => event.event === "test.flush.drain")
110 .map((event) => event.properties?.index)
111 .filter((index): index is number => typeof index === "number"),
112 );
91 const batchRequests = capturedRequests.filter(
92 (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>
93 Array.isArray(request.body?.batch),
94 );
95 assert.equal(batchRequests.length, 3);
96 assert.equal(
97 batchRequests.every((request) => request.path === "/batch/" || request.path === "/batch"),
98 true,
99 );
100 const deliveredIndexes = batchRequests.flatMap((request) =>
101 request.body.batch
102 .filter((event) => event.event === "test.flush.drain")
103 .map((event) => event.properties?.index)
104 .filter((index): index is number => typeof index === "number"),
105 );
113106
114 const sorted = deliveredIndexes.toSorted((a, b) => a - b);
115 assert.equal(sorted.length, 45);
116 assert.deepEqual(
117 sorted,
118 Array.from({ length: 45 }, (_, index) => index),
119 );
120 }),
121);
107 const sorted = deliveredIndexes.toSorted((a, b) => a - b);
108 assert.equal(sorted.length, 45);
109 assert.deepEqual(
110 sorted,
111 Array.from({ length: 45 }, (_, index) => index),
112 );
113 }),
114 );
115});