1import * as AWS from "@/AWS";
2import { SamplingRule } from "@/AWS/XRay";
3import * as Test from "@/Test/Alchemy";
4import * as xray from "@distilled.cloud/aws/xray";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Option from "effect/Option";
9import * as Schedule from "effect/Schedule";
10import * as Stream from "effect/Stream";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14const findRule = (ruleName: string) =>
15 xray.getSamplingRules.items({}).pipe(
16 Stream.filter((record) => record.SamplingRule?.RuleName === ruleName),
17 Stream.runHead,
18 Effect.map((record) => Option.getOrUndefined(record)?.SamplingRule),
19 );
20
21class SamplingRuleStillExists extends Data.TaggedError(
22 "SamplingRuleStillExists",
23)<{ readonly ruleName: string }> {}
24
25const assertRuleDeleted = (ruleName: string) =>
26 findRule(ruleName).pipe(
27 Effect.flatMap((rule) =>
28 rule === undefined
29 ? Effect.void
30 : Effect.fail(new SamplingRuleStillExists({ ruleName })),
31 ),
32 Effect.retry({
33 while: (e) => e._tag === "SamplingRuleStillExists",
34 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
35 }),
36 );
37
38test.provider(
39 "create, update, no-op, delete sampling rule",
40 (stack) =>
41 Effect.gen(function* () {
42 yield* stack.destroy();
43
44 const rule = yield* stack.deploy(
45 Effect.gen(function* () {
46 return yield* SamplingRule("TestRule", {
47 priority: 9000,
48 fixedRate: 0.05,
49 reservoirSize: 1,
50 serviceName: "alchemy-xray-test-*",
51 tags: { Environment: "test" },
52 });
53 }),
54 );
55
56 expect(rule.ruleName).toBeDefined();
57 expect(rule.ruleArn).toContain(":sampling-rule/");
58
59 // out-of-band verification via distilled
60 const created = yield* findRule(rule.ruleName);
61 expect(created?.Priority).toBe(9000);
62 expect(created?.FixedRate).toBe(0.05);
63 expect(created?.ReservoirSize).toBe(1);
64 expect(created?.ServiceName).toBe("alchemy-xray-test-*");
65 expect(created?.ServiceType).toBe("*");
66 const tags = yield* xray
67 .listTagsForResource({ ResourceARN: rule.ruleArn })
68 .pipe(
69 Effect.map((r) =>
70 Object.fromEntries((r.Tags ?? []).map((t) => [t.Key, t.Value])),
71 ),
72 );
73 expect(tags.Environment).toBe("test");
74 expect(tags["alchemy::id"]).toBe("TestRule");
75
76 // update mutable fields (priority, rates, matchers, attributes, tags)
77 const updated = yield* stack.deploy(
78 Effect.gen(function* () {
79 return yield* SamplingRule("TestRule", {
80 priority: 8000,
81 fixedRate: 0.1,
82 reservoirSize: 2,
83 serviceName: "alchemy-xray-test-*",
84 httpMethod: "GET",
85 attributes: { tier: "premium" },
86 tags: { Environment: "test", Extra: "1" },
87 });
88 }),
89 );
90 expect(updated.ruleName).toBe(rule.ruleName);
91 expect(updated.ruleArn).toBe(rule.ruleArn);
92
93 const afterUpdate = yield* findRule(rule.ruleName);
94 expect(afterUpdate?.Priority).toBe(8000);
95 expect(afterUpdate?.FixedRate).toBe(0.1);
96 expect(afterUpdate?.ReservoirSize).toBe(2);
97 expect(afterUpdate?.HTTPMethod).toBe("GET");
98 expect(afterUpdate?.Attributes).toEqual({ tier: "premium" });
99 const updatedTags = yield* xray
100 .listTagsForResource({ ResourceARN: rule.ruleArn })
101 .pipe(
102 Effect.map((r) =>
103 Object.fromEntries((r.Tags ?? []).map((t) => [t.Key, t.Value])),
104 ),
105 );
106 expect(updatedTags.Extra).toBe("1");
107
108 // remove a tag — converges via untagResource
109 yield* stack.deploy(
110 Effect.gen(function* () {
111 return yield* SamplingRule("TestRule", {
112 priority: 8000,
113 fixedRate: 0.1,
114 reservoirSize: 2,
115 serviceName: "alchemy-xray-test-*",
116 httpMethod: "GET",
117 attributes: { tier: "premium" },
118 tags: { Environment: "test" },
119 });
120 }),
121 );
122 const afterTagRemoval = yield* xray
123 .listTagsForResource({ ResourceARN: rule.ruleArn })
124 .pipe(
125 Effect.map((r) =>
126 Object.fromEntries((r.Tags ?? []).map((t) => [t.Key, t.Value])),
127 ),
128 );
129 expect(afterTagRemoval.Extra).toBeUndefined();
130 expect(afterTagRemoval.Environment).toBe("test");
131
132 yield* stack.destroy();
133 yield* assertRuleDeleted(rule.ruleName);
134 }),
135 { tags: ["provider:aws", "provider:aws:xray", "live"], timeout: 120_000 },
136);
137
138test.provider(
139 "custom rule name and replacement on rename",
140 (stack) =>
141 Effect.gen(function* () {
142 yield* stack.destroy();
143
144 const first = yield* stack.deploy(
145 Effect.gen(function* () {
146 return yield* SamplingRule("NamedRule", {
147 ruleName: "alchemy-test-rule-a",
148 priority: 9100,
149 fixedRate: 0.01,
150 });
151 }),
152 );
153 expect(first.ruleName).toBe("alchemy-test-rule-a");
154 expect(first.ruleArn).toContain(":sampling-rule/alchemy-test-rule-a");
155
156 // renaming triggers a replacement: new physical rule, old one gone
157 const second = yield* stack.deploy(
158 Effect.gen(function* () {
159 return yield* SamplingRule("NamedRule", {
160 ruleName: "alchemy-test-rule-b",
161 priority: 9100,
162 fixedRate: 0.01,
163 });
164 }),
165 );
166 expect(second.ruleName).toBe("alchemy-test-rule-b");
167
168 const observed = yield* findRule("alchemy-test-rule-b");
169 expect(observed?.Priority).toBe(9100);
170 yield* assertRuleDeleted("alchemy-test-rule-a");
171
172 yield* stack.destroy();
173 yield* assertRuleDeleted("alchemy-test-rule-b");
174 }),
175 { tags: ["provider:aws", "provider:aws:xray", "live"], timeout: 120_000 },
176);