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 @@...@@ -1,10 +1,7 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
5import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";1import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
2import * as NodeServices from "@effect/platform-node/NodeServices";
6import { assert, it } from "@effect/vitest";3import { assert, it } from "@effect/vitest";
7import { ConfigProvider, Effect, Layer } from "effect";4import { ConfigProvider, Effect, FileSystem, Layer } from "effect";
8import * as HttpServer from "effect/unstable/http/HttpServer";5import * as HttpServer from "effect/unstable/http/HttpServer";
9import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";6import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
10import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";7import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
...@@ -16,16 +13,14 @@ import { AnalyticsServiceLayerLive } from "./AnalyticsService.ts";...@@ -16,16 +13,14 @@ import { AnalyticsServiceLayerLive } from "./AnalyticsService.ts";
1613
17interface RecordedBatchRequest {14interface RecordedBatchRequest {
18 readonly path: string;15 readonly path: string;
19 readonly body:16 readonly body: {
20 | {17 readonly batch?: ReadonlyArray<{
21 readonly batch?: ReadonlyArray<{18 readonly event?: string;
22 readonly event?: string;19 readonly properties?: {
23 readonly properties?: {20 readonly index?: number;
24 readonly index?: number;21 };
25 };22 }>;
26 }>;23 } | null;
27 }
28 | null;
29}24}
3025
31interface RecordedBatchBody {26interface RecordedBatchBody {
...@@ -37,85 +32,84 @@ interface RecordedBatchBody {...@@ -37,85 +32,84 @@ interface RecordedBatchBody {
37 }>;32 }>;
38}33}
3934
40it.effect("flush drains all buffered events across multiple batches", () =>35it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
41 Effect.gen(function* () {36 it.effect("flush drains all buffered events across multiple batches", () =>
42 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-telemetry-flush-"));37 Effect.gen(function* () {
43 const capturedRequests: Array<RecordedBatchRequest> = [];38 const fileSystem = yield* FileSystem.FileSystem;
44 const telemetryLayer = AnalyticsServiceLayerLive.pipe(39 const stateDir = yield* fileSystem.makeTempDirectoryScoped({
45 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), tempDir)),40 prefix: "t3-telemetry-flush-",
46 );41 });
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 });
6942
70 return HttpServerResponse.jsonUnsafe({});43 const capturedRequests: Array<RecordedBatchRequest> = [];
71 }),44 const serverConfigLayer = ServerConfig.layerTest(process.cwd(), stateDir);
72 );
73 const runtimeLayer = telemetryLayer.pipe(
74 Layer.provide(configLayer),
75 Layer.provideMerge(NodeHttpServer.layerTest),
76 );
7745
78 yield* Effect.gen(function* () {46 const telemetryLayer = AnalyticsServiceLayerLive.pipe(Layer.provideMerge(serverConfigLayer));
79 yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped);47 const configLayer = ConfigProvider.layer(
80 const telemetryIdentifier = yield* getTelemetryIdentifier;48 ConfigProvider.fromUnknown({
81 assert.equal(telemetryIdentifier !== null, true);49 T3CODE_TELEMETRY_ENABLED: true,
82 const analytics = yield* AnalyticsService;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) {62 const payload = yield* request.json.pipe(
85 yield* analytics.record("test.flush.drain", { index });63 Effect.map((body) => body as RecordedBatchRequest["body"]),
86 }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;70 return HttpServerResponse.jsonUnsafe({});
89 }).pipe(
90 Effect.provide(runtimeLayer),
91 Effect.ensuring(
92 Effect.sync(() => {
93 fs.rmSync(tempDir, { recursive: true, force: true });
94 }),71 }),
95 ),72 );
96 );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(91 const batchRequests = capturedRequests.filter(
99 (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>92 (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>
100 Array.isArray(request.body?.batch),93 Array.isArray(request.body?.batch),
101 );94 );
102 assert.equal(batchRequests.length, 3);95 assert.equal(batchRequests.length, 3);
103 assert.equal(96 assert.equal(
104 batchRequests.every((request) => request.path === "/batch/" || request.path === "/batch"),97 batchRequests.every((request) => request.path === "/batch/" || request.path === "/batch"),
105 true,98 true,
106 );99 );
107 const deliveredIndexes = batchRequests.flatMap((request) =>100 const deliveredIndexes = batchRequests.flatMap((request) =>
108 request.body.batch101 request.body.batch
109 .filter((event) => event.event === "test.flush.drain")102 .filter((event) => event.event === "test.flush.drain")
110 .map((event) => event.properties?.index)103 .map((event) => event.properties?.index)
111 .filter((index): index is number => typeof index === "number"),104 .filter((index): index is number => typeof index === "number"),
112 );105 );
113106
114 const sorted = deliveredIndexes.toSorted((a, b) => a - b);107 const sorted = deliveredIndexes.toSorted((a, b) => a - b);
115 assert.equal(sorted.length, 45);108 assert.equal(sorted.length, 45);
116 assert.deepEqual(109 assert.deepEqual(
117 sorted,110 sorted,
118 Array.from({ length: 45 }, (_, index) => index),111 Array.from({ length: 45 }, (_, index) => index),
119 );112 );
120 }),113 }),
121);114 );
115});