1import * as Lambda from "@/AWS/Lambda";
2import * as XRay from "@/AWS/XRay";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Result from "effect/Result";
7import * as Schedule from "effect/Schedule";
8import * as Stream from "effect/Stream";
9import { HttpServerRequest } from "effect/http/HttpServerRequest";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11import path from "pathe";
12
13const main = path.resolve(import.meta.dirname, "handler.ts");
14
15export class XRayTestFunction extends Lambda.Function<Lambda.Function>()(
16 "XRayTestFunction",
17) {}
18
19/** Collapse a typed API call to `"ok"` or its typed error tag. */
20const outcome = <A, E extends { _tag: string }>(effect: Effect.Effect<A, E>) =>
21 Effect.result(effect).pipe(
22 Effect.map((result) =>
23 Result.isFailure(result) ? result.failure._tag : "ok",
24 ),
25 );
26
27export default XRayTestFunction.make(
28 {
29 main,
30 functionUrl: true,
31 // Active tracing: every sampled invocation of this function produces an
32 // X-Ray trace whose service name is the function name.
33 tracing: "Active",
34 timeout: Duration.seconds(30),
35 },
36 Effect.gen(function* () {
37 // Event source: subscribe the host to X-Ray insight updates. The deploy
38 // proves the EventBridge rule + invoke permission wiring.
39 yield* XRay.consumeInsightEvents({ states: ["ACTIVE"] }, (events) =>
40 Stream.runForEach(events, (event) =>
41 Effect.log(
42 `xray insight: ${event.detail.InsightId} (${event.detail.State})`,
43 ),
44 ),
45 );
46
47 const getTraceSummaries = yield* XRay.GetTraceSummaries();
48 const batchGetTraces = yield* XRay.BatchGetTraces();
49 const putTraceSegments = yield* XRay.PutTraceSegments();
50 const putTelemetryRecords = yield* XRay.PutTelemetryRecords();
51 const getSamplingRules = yield* XRay.GetSamplingRules();
52 const getSamplingTargets = yield* XRay.GetSamplingTargets();
53 const getSamplingStatisticSummaries =
54 yield* XRay.GetSamplingStatisticSummaries();
55 const getServiceGraph = yield* XRay.GetServiceGraph();
56 const getTraceGraph = yield* XRay.GetTraceGraph();
57 const getTimeSeriesServiceStatistics =
58 yield* XRay.GetTimeSeriesServiceStatistics();
59 const getInsight = yield* XRay.GetInsight();
60 const getInsightEvents = yield* XRay.GetInsightEvents();
61 const getInsightImpactGraph = yield* XRay.GetInsightImpactGraph();
62 const getInsightSummaries = yield* XRay.GetInsightSummaries();
63 const getTraceSegmentDestination = yield* XRay.GetTraceSegmentDestination();
64 const startTraceRetrieval = yield* XRay.StartTraceRetrieval();
65 const listRetrievedTraces = yield* XRay.ListRetrievedTraces();
66 const getRetrievedTracesGraph = yield* XRay.GetRetrievedTracesGraph();
67 const cancelTraceRetrieval = yield* XRay.CancelTraceRetrieval();
68
69 // X-Ray read APIs have low TPS quotas; the test polls routes while
70 // sibling suites run, so absorb throttling in the fixture.
71 const throttleRetry = <A, E extends { _tag: string }>(
72 effect: Effect.Effect<A, E>,
73 ) =>
74 effect.pipe(
75 Effect.retry({
76 while: (error): boolean => error._tag === "ThrottledException",
77 schedule: Schedule.max([
78 Schedule.exponential("500 millis"),
79 Schedule.recurs(5),
80 ]),
81 }),
82 );
83
84 return {
85 fetch: Effect.gen(function* () {
86 const request = yield* HttpServerRequest;
87 const url = new URL(request.originalUrl);
88 const pathname = url.pathname;
89 const now = yield* Effect.sync(() => Date.now());
90
91 if (request.method === "GET" && pathname === "/ping") {
92 const functionName = yield* Effect.sync(
93 () => process.env.AWS_LAMBDA_FUNCTION_NAME,
94 );
95 return yield* HttpServerResponse.json({ ok: true, functionName });
96 }
97
98 if (request.method === "GET" && pathname === "/trace-summaries") {
99 const service = url.searchParams.get("service");
100 const result = yield* throttleRetry(
101 getTraceSummaries({
102 StartTime: new Date(now - 10 * 60 * 1000),
103 EndTime: new Date(now),
104 FilterExpression: service ? `service("${service}")` : undefined,
105 }),
106 );
107 return yield* HttpServerResponse.json({
108 traceIds: (result.TraceSummaries ?? []).flatMap((summary) =>
109 summary.Id ? [summary.Id] : [],
110 ),
111 });
112 }
113
114 if (request.method === "GET" && pathname === "/batch-get-traces") {
115 const ids = url.searchParams.get("ids");
116 const result = yield* throttleRetry(
117 batchGetTraces({ TraceIds: ids ? ids.split(",") : [] }),
118 );
119 return yield* HttpServerResponse.json({
120 traces: (result.Traces ?? []).map((trace) => ({
121 id: trace.Id,
122 segments: trace.Segments?.length ?? 0,
123 })),
124 unprocessed: result.UnprocessedTraceIds ?? [],
125 });
126 }
127
128 if (request.method === "POST" && pathname === "/put-trace-segments") {
129 // A well-formed custom segment: epoch-anchored trace id, unique
130 // segment id, sub-second duration.
131 const epochHex = Math.floor(now / 1000).toString(16);
132 const random24 = Array.from({ length: 24 }, () =>
133 Math.floor(Math.random() * 16).toString(16),
134 ).join("");
135 const segmentId = Array.from({ length: 16 }, () =>
136 Math.floor(Math.random() * 16).toString(16),
137 ).join("");
138 const traceId = `1-${epochHex}-${random24}`;
139 const result = yield* throttleRetry(
140 putTraceSegments({
141 TraceSegmentDocuments: [
142 JSON.stringify({
143 name: "alchemy-xray-binding-test",
144 id: segmentId,
145 trace_id: traceId,
146 start_time: now / 1000 - 0.5,
147 end_time: now / 1000,
148 }),
149 ],
150 }),
151 );
152 return yield* HttpServerResponse.json({
153 traceId,
154 unprocessed: result.UnprocessedTraceSegments ?? [],
155 });
156 }
157
158 if (request.method === "POST" && pathname === "/telemetry") {
159 yield* throttleRetry(
160 putTelemetryRecords({
161 TelemetryRecords: [
162 {
163 Timestamp: new Date(now),
164 SegmentsReceivedCount: 1,
165 SegmentsSentCount: 1,
166 },
167 ],
168 }),
169 );
170 return yield* HttpServerResponse.json({ ok: true });
171 }
172
173 if (request.method === "GET" && pathname === "/sampling") {
174 const rules = yield* throttleRetry(getSamplingRules());
175 const summaries = yield* throttleRetry(
176 getSamplingStatisticSummaries(),
177 );
178 // Report sampling statistics for the built-in Default rule — the
179 // real sampler protocol handshake.
180 const clientId = Array.from({ length: 24 }, () =>
181 Math.floor(Math.random() * 16).toString(16),
182 ).join("");
183 const targets = yield* Effect.result(
184 getSamplingTargets({
185 SamplingStatisticsDocuments: [
186 {
187 RuleName: "Default",
188 ClientID: clientId,
189 Timestamp: new Date(now),
190 RequestCount: 1,
191 SampledCount: 1,
192 },
193 ],
194 }),
195 );
196 return yield* HttpServerResponse.json({
197 ruleNames: (rules.SamplingRuleRecords ?? []).flatMap((record) =>
198 record.SamplingRule?.RuleName
199 ? [record.SamplingRule.RuleName]
200 : [],
201 ),
202 statisticSummaries: (summaries.SamplingStatisticSummaries ?? [])
203 .length,
204 targets: Result.isSuccess(targets)
205 ? {
206 outcome: "ok",
207 documents: (targets.success.SamplingTargetDocuments ?? [])
208 .length,
209 }
210 : { outcome: targets.failure._tag, documents: null },
211 });
212 }
213
214 if (request.method === "GET" && pathname === "/service-graph") {
215 const graph = yield* Effect.result(
216 throttleRetry(
217 getServiceGraph({
218 StartTime: new Date(now - 10 * 60 * 1000),
219 EndTime: new Date(now),
220 }),
221 ),
222 );
223 return yield* HttpServerResponse.json(
224 Result.isSuccess(graph)
225 ? { services: (graph.success.Services ?? []).length, error: null }
226 : { services: null, error: graph.failure._tag },
227 );
228 }
229
230 if (request.method === "GET" && pathname === "/trace-graph") {
231 const ids = url.searchParams.get("ids");
232 const graph = yield* Effect.result(
233 throttleRetry(
234 getTraceGraph({ TraceIds: ids ? ids.split(",") : [] }),
235 ),
236 );
237 return yield* HttpServerResponse.json(
238 Result.isSuccess(graph)
239 ? { services: (graph.success.Services ?? []).length, error: null }
240 : { services: null, error: graph.failure._tag },
241 );
242 }
243
244 if (request.method === "GET" && pathname === "/time-series") {
245 const service = url.searchParams.get("service");
246 const stats = yield* Effect.result(
247 getTimeSeriesServiceStatistics({
248 StartTime: new Date(now - 10 * 60 * 1000),
249 EndTime: new Date(now),
250 EntitySelectorExpression: service
251 ? `service("${service}")`
252 : undefined,
253 Period: 60,
254 }),
255 );
256 return yield* HttpServerResponse.json(
257 Result.isSuccess(stats)
258 ? {
259 points: (stats.success.TimeSeriesServiceStatistics ?? [])
260 .length,
261 error: null,
262 }
263 : { points: null, error: stats.failure._tag },
264 );
265 }
266
267 if (request.method === "GET" && pathname === "/insight-summaries") {
268 // The group may not have insights enabled — report either the
269 // summary count or the API's typed error tag.
270 const group = url.searchParams.get("group") ?? "Default";
271 const result = yield* Effect.result(
272 throttleRetry(
273 getInsightSummaries({
274 GroupName: group,
275 StartTime: new Date(now - 24 * 60 * 60 * 1000),
276 EndTime: new Date(now),
277 }),
278 ),
279 );
280 return yield* HttpServerResponse.json(
281 Result.isSuccess(result)
282 ? {
283 insights: (result.success.InsightSummaries ?? []).length,
284 error: null,
285 }
286 : { insights: null, error: result.failure._tag },
287 );
288 }
289
290 if (request.method === "GET" && pathname === "/insight") {
291 // No insight can be provisioned on demand — prove each binding's
292 // IAM grant by observing the API's typed validation error for a
293 // syntactically-plausible but nonexistent insight id (an IAM
294 // failure would surface as AccessDeniedException instead).
295 const insightId =
296 url.searchParams.get("id") ??
297 "00000000-0000-0000-0000-000000000000";
298 return yield* HttpServerResponse.json({
299 getInsight: yield* outcome(getInsight({ InsightId: insightId })),
300 getInsightEvents: yield* outcome(
301 getInsightEvents({ InsightId: insightId }),
302 ),
303 getInsightImpactGraph: yield* outcome(
304 getInsightImpactGraph({
305 InsightId: insightId,
306 StartTime: new Date(now - 60 * 60 * 1000),
307 EndTime: new Date(now),
308 }),
309 ),
310 });
311 }
312
313 if (request.method === "GET" && pathname === "/trace-retrieval") {
314 // Transaction Search requires a CloudWatch Logs trace destination;
315 // on the default X-Ray destination the retrieval ops answer with
316 // typed errors — either way each call proves its IAM grant.
317 const destination = yield* throttleRetry(
318 getTraceSegmentDestination(),
319 );
320 const epochHex = Math.floor(now / 1000).toString(16);
321 const start = yield* Effect.result(
322 startTraceRetrieval({
323 TraceIds: [`1-${epochHex}-abcdef0123456789abcdef01`],
324 StartTime: new Date(now - 60 * 60 * 1000),
325 EndTime: new Date(now),
326 }),
327 );
328 const retrievalToken = Result.isSuccess(start)
329 ? start.success.RetrievalToken
330 : undefined;
331 const bogusToken = retrievalToken ?? "alchemy-nonexistent-token";
332 return yield* HttpServerResponse.json({
333 destination: destination.Destination,
334 startTraceRetrieval: Result.isFailure(start)
335 ? start.failure._tag
336 : "ok",
337 listRetrievedTraces: yield* outcome(
338 listRetrievedTraces({ RetrievalToken: bogusToken }),
339 ),
340 getRetrievedTracesGraph: yield* outcome(
341 getRetrievedTracesGraph({ RetrievalToken: bogusToken }),
342 ),
343 cancelTraceRetrieval: yield* outcome(
344 cancelTraceRetrieval({ RetrievalToken: bogusToken }),
345 ),
346 });
347 }
348
349 return yield* HttpServerResponse.json(
350 { error: "Not found", method: request.method, pathname },
351 { status: 404 },
352 );
353 }).pipe(Effect.orDie),
354 };
355 }).pipe(
356 Effect.provide(
357 Layer.mergeAll(
358 Lambda.EventSource,
359 XRay.GetTraceSummariesHttp,
360 XRay.BatchGetTracesHttp,
361 XRay.PutTraceSegmentsHttp,
362 XRay.PutTelemetryRecordsHttp,
363 XRay.GetSamplingRulesHttp,
364 XRay.GetSamplingTargetsHttp,
365 XRay.GetSamplingStatisticSummariesHttp,
366 XRay.GetServiceGraphHttp,
367 XRay.GetTraceGraphHttp,
368 XRay.GetTimeSeriesServiceStatisticsHttp,
369 XRay.GetInsightHttp,
370 XRay.GetInsightEventsHttp,
371 XRay.GetInsightImpactGraphHttp,
372 XRay.GetInsightSummariesHttp,
373 XRay.GetTraceSegmentDestinationHttp,
374 XRay.StartTraceRetrievalHttp,
375 XRay.ListRetrievedTracesHttp,
376 XRay.GetRetrievedTracesGraphHttp,
377 XRay.CancelTraceRetrievalHttp,
378 ),
379 ),
380 ),
381);