1import * as AWS from "@/AWS";
2import { AppMonitor } from "@/AWS/RUM";
3import * as Test from "@/Test/Alchemy";
4import * as rum from "@distilled.cloud/aws/rum";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const findMonitor = (name: string) =>
13 rum.getAppMonitor({ Name: name }).pipe(
14 Effect.map((r) => r.AppMonitor),
15 Effect.catchTag("ResourceNotFoundException", () =>
16 Effect.succeed(undefined),
17 ),
18 );
19
20class AppMonitorStillExists extends Data.TaggedError("AppMonitorStillExists")<{
21 readonly name: string;
22}> {}
23
24const assertMonitorDeleted = (name: string) =>
25 findMonitor(name).pipe(
26 Effect.flatMap((monitor) =>
27 monitor === undefined
28 ? Effect.void
29 : Effect.fail(new AppMonitorStillExists({ name })),
30 ),
31 Effect.retry({
32 while: (e) => e._tag === "AppMonitorStillExists",
33 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
34 }),
35 );
36
37// Ungated typed-error probe: prove the distilled error union carries the
38// not-found tag this provider's read/delete paths depend on.
39test.provider(
40 "getAppMonitor on a nonexistent monitor fails with ResourceNotFoundException",
41 () =>
42 Effect.gen(function* () {
43 const error = yield* Effect.flip(
44 rum.getAppMonitor({ Name: "alchemy-nonexistent-rum-monitor-probe" }),
45 );
46 expect(error._tag).toBe("ResourceNotFoundException");
47 }),
48 { tags: ["provider:aws", "provider:aws:rum", "live"] },
49);
50
51test.provider(
52 "create, update in place, delete app monitor",
53 (stack) =>
54 Effect.gen(function* () {
55 yield* stack.destroy();
56
57 const monitor = yield* stack.deploy(
58 Effect.gen(function* () {
59 return yield* AppMonitor("TestMonitor", {
60 domain: "example.com",
61 appMonitorConfiguration: {
62 sessionSampleRate: 0.5,
63 telemetries: ["errors", "http"],
64 allowCookies: true,
65 },
66 tags: { fixture: "rum-app-monitor" },
67 });
68 }),
69 );
70
71 expect(monitor.appMonitorName).toBeDefined();
72 expect(monitor.appMonitorId).toBeDefined();
73 expect(monitor.appMonitorArn).toContain(":appmonitor/");
74
75 // out-of-band verification via distilled
76 const created = yield* findMonitor(monitor.appMonitorName);
77 expect(created?.Id).toBe(monitor.appMonitorId);
78 expect(created?.Domain).toBe("example.com");
79 expect(created?.AppMonitorConfiguration?.SessionSampleRate).toBe(0.5);
80 expect(
81 [...(created?.AppMonitorConfiguration?.Telemetries ?? [])].sort(),
82 ).toEqual(["errors", "http"]);
83 expect(created?.AppMonitorConfiguration?.AllowCookies).toBe(true);
84 expect(created?.DataStorage?.CwLog?.CwLogEnabled ?? false).toBe(false);
85 expect(created?.CustomEvents?.Status ?? "DISABLED").toBe("DISABLED");
86 expect(created?.Tags?.fixture).toBe("rum-app-monitor");
87 expect(created?.Tags?.["alchemy::id"]).toBe("TestMonitor");
88
89 // update in place: domain, sample rate, cw logs, custom events, tags
90 const updated = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* AppMonitor("TestMonitor", {
93 domain: "updated.example.com",
94 appMonitorConfiguration: {
95 sessionSampleRate: 1,
96 telemetries: ["errors"],
97 allowCookies: false,
98 },
99 cwLogEnabled: true,
100 customEvents: "ENABLED",
101 tags: { fixture: "rum-app-monitor", phase: "two" },
102 });
103 }),
104 );
105 // same physical monitor — update, not replace
106 expect(updated.appMonitorName).toBe(monitor.appMonitorName);
107 expect(updated.appMonitorId).toBe(monitor.appMonitorId);
108
109 const afterUpdate = yield* findMonitor(monitor.appMonitorName);
110 expect(afterUpdate?.Domain).toBe("updated.example.com");
111 expect(afterUpdate?.AppMonitorConfiguration?.SessionSampleRate).toBe(1);
112 expect(afterUpdate?.AppMonitorConfiguration?.Telemetries).toEqual([
113 "errors",
114 ]);
115 expect(afterUpdate?.AppMonitorConfiguration?.AllowCookies).toBe(false);
116 expect(afterUpdate?.DataStorage?.CwLog?.CwLogEnabled).toBe(true);
117 expect(afterUpdate?.CustomEvents?.Status).toBe("ENABLED");
118 expect(afterUpdate?.Tags?.phase).toBe("two");
119
120 // removing a user tag converges (internal tags survive)
121 yield* stack.deploy(
122 Effect.gen(function* () {
123 return yield* AppMonitor("TestMonitor", {
124 domain: "updated.example.com",
125 appMonitorConfiguration: {
126 sessionSampleRate: 1,
127 telemetries: ["errors"],
128 allowCookies: false,
129 },
130 cwLogEnabled: true,
131 customEvents: "ENABLED",
132 tags: { fixture: "rum-app-monitor" },
133 });
134 }),
135 );
136 const afterTagRemoval = yield* findMonitor(monitor.appMonitorName);
137 expect(afterTagRemoval?.Tags?.phase).toBeUndefined();
138 expect(afterTagRemoval?.Tags?.fixture).toBe("rum-app-monitor");
139 expect(afterTagRemoval?.Tags?.["alchemy::id"]).toBe("TestMonitor");
140
141 yield* stack.destroy();
142 yield* assertMonitorDeleted(monitor.appMonitorName);
143 }),
144 { tags: ["provider:aws", "provider:aws:rum", "live"], timeout: 120_000 },
145);
146
147test.provider(
148 "custom name, domainList, and replacement on rename",
149 (stack) =>
150 Effect.gen(function* () {
151 yield* stack.destroy();
152
153 const first = yield* stack.deploy(
154 Effect.gen(function* () {
155 return yield* AppMonitor("NamedMonitor", {
156 appMonitorName: "alchemy-test-rum-monitor-a",
157 domainList: ["example.com", "app.example.com"],
158 });
159 }),
160 );
161 expect(first.appMonitorName).toBe("alchemy-test-rum-monitor-a");
162
163 const observed = yield* findMonitor(first.appMonitorName);
164 expect([...(observed?.DomainList ?? [])].sort()).toEqual([
165 "app.example.com",
166 "example.com",
167 ]);
168
169 // rename replaces the monitor
170 const second = yield* stack.deploy(
171 Effect.gen(function* () {
172 return yield* AppMonitor("NamedMonitor", {
173 appMonitorName: "alchemy-test-rum-monitor-b",
174 domainList: ["example.com", "app.example.com"],
175 });
176 }),
177 );
178 expect(second.appMonitorName).toBe("alchemy-test-rum-monitor-b");
179 expect(second.appMonitorId).not.toBe(first.appMonitorId);
180
181 // the old monitor is cleaned up by the replacement
182 yield* assertMonitorDeleted("alchemy-test-rum-monitor-a");
183
184 yield* stack.destroy();
185 yield* assertMonitorDeleted("alchemy-test-rum-monitor-b");
186 }),
187 { tags: ["provider:aws", "provider:aws:rum", "live"], timeout: 120_000 },
188);
189
190test.provider(
191 "rejects a monitor with both domain and domainList",
192 (stack) =>
193 Effect.gen(function* () {
194 yield* stack.destroy();
195 const result = yield* Effect.result(
196 stack.deploy(
197 Effect.gen(function* () {
198 return yield* AppMonitor("BadMonitor", {
199 domain: "example.com",
200 domainList: ["other.example.com"],
201 });
202 }),
203 ),
204 );
205 expect(result._tag).toBe("Failure");
206 yield* stack.destroy();
207 }),
208 { tags: ["provider:aws", "provider:aws:rum", "live"], timeout: 60_000 },
209);