1import * as Axiom from "@distilled.cloud/axiom";
2import * as Effect from "effect/Effect";
3import * as Provider from "../Provider.ts";
4import { Resource } from "../Resource.ts";
5import type { Providers } from "./Providers.ts";
6
7export type AnnotationProps = Axiom.CreateAnnotationRequest;
8
9export type Annotation = Resource<
10 "Axiom.Annotation",
11 AnnotationProps,
12 Axiom.Annotation,
13 never,
14 Providers
15>;
16
17/**
18 * An Axiom annotation — a vertical marker overlaid on charts to flag a
19 * deploy, incident, feature flag flip, or any other point/range event you
20 * want correlated with telemetry.
21 *
22 * Annotations are scoped to one or more `datasets`. Use a single `time` for
23 * a point marker or `time` + `endTime` for a range. The `type` (e.g.
24 * `"deploy"`, `"incident"`) groups markers visually in the UI.
25 *
26 * Although typically created at deploy/release time (out-of-band of
27 * regular IaC), modelling them as resources makes per-environment
28 * annotation history reproducible.
29 * @see https://axiom.co/docs/query-data/annotate-charts
30 *
31 * ### Creating an Annotation
32 * **Example:** Point-in-time deploy marker
33 * ```typescript
34 * yield* Axiom.Annotation("deploy-1.2.3", {
35 * type: "deploy",
36 * title: "Release 1.2.3",
37 * description: "https://github.com/acme/app/releases/tag/v1.2.3",
38 * datasets: ["my-app-traces", "my-app-logs"],
39 * time: new Date().toISOString(),
40 * url: "https://github.com/acme/app/releases/tag/v1.2.3",
41 * });
42 * ```
43 *
44 * **Example:** Incident time-range
45 * ```typescript
46 * yield* Axiom.Annotation("inc-2026-04-27", {
47 * type: "incident",
48 * title: "Database failover",
49 * datasets: ["my-app-traces"],
50 * time: "2026-04-27T18:05:00Z",
51 * endTime: "2026-04-27T18:32:00Z",
52 * url: "https://incident.io/incidents/abc123",
53 * });
54 * ```
55 *
56 * @resource
57 * @product Dataset
58 */
59export const Annotation = Resource<Annotation>("Axiom.Annotation");
60
61export const AnnotationProvider = () =>
62 Provider.effect(
63 Annotation,
64 Effect.gen(function* () {
65 const create = yield* Axiom.createAnnotation;
66 const update = yield* Axiom.updateAnnotation;
67 const get = yield* Axiom.getAnnotation;
68 const del = yield* Axiom.deleteAnnotation;
69 const listAnnotations = yield* Axiom.getAnnotations;
70
71 return {
72 stables: ["id"],
73 // Axiom exposes a flat, account-wide `GET /v2/annotations` list that
74 // returns every annotation's full record in one (non-paginated)
75 // response — the exact same shape `get`/`read` produce. Hand each
76 // record back directly as the resource's Attributes.
77 list: () =>
78 listAnnotations({}).pipe(
79 Effect.map((annotations) => annotations.map((a) => ({ ...a }))),
80 ),
81 reconcile: Effect.fn(function* ({ news, output }) {
82 // Observe — Axiom assigns the annotation id server-side, so the
83 // only handle to a previously-created annotation is the cached
84 // `output.id`. Probe for live state with that id; treat NotFound
85 // (deleted out-of-band, or never created) as "no observed state".
86 const observed = output?.id
87 ? yield* get({ id: output.id }).pipe(
88 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
89 )
90 : undefined;
91
92 // Ensure — when no observed annotation exists, POST creates one
93 // and Axiom assigns the id.
94 if (observed === undefined) {
95 return yield* create(news);
96 }
97
98 // Sync — the annotation exists; apply desired props with PUT and
99 // preserve the stable id and original time as fallbacks.
100 const result = yield* update({ ...news, id: observed.id! });
101 return {
102 ...result,
103 id: observed.id!,
104 time: result.time ?? output?.time ?? news.time,
105 };
106 }),
107 delete: Effect.fn(function* ({ output }) {
108 yield* del({ id: output.id }).pipe(
109 Effect.catchTag("NotFound", () => Effect.void),
110 );
111 }),
112 read: Effect.fn(function* ({ output }) {
113 if (!output?.id) return undefined;
114 return yield* get({ id: output.id }).pipe(
115 Effect.map((current) => ({
116 ...current,
117 id: current.id ?? output.id,
118 time: current.time ?? output.time,
119 })),
120 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
121 );
122 }),
123 };
124 }),
125 );