blogwright-analytics 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/dist/adapters/duckdb-ingest.d.ts +76 -0
- package/dist/adapters/duckdb-ingest.js +173 -0
- package/dist/adapters/duckdb-query.d.ts +56 -0
- package/dist/adapters/duckdb-query.js +80 -0
- package/dist/adapters/duckdb-session.d.ts +168 -0
- package/dist/adapters/duckdb-session.js +330 -0
- package/dist/app/_app/immutable/assets/0.BTQrrh5B.css +1 -0
- package/dist/app/_app/immutable/assets/2.CZSK3rT8.css +1 -0
- package/dist/app/_app/immutable/assets/BrushContext.D7c8UPey.css +1 -0
- package/dist/app/_app/immutable/assets/ChartAnnotations.CPxIG7Mw.css +1 -0
- package/dist/app/_app/immutable/assets/Circle.C5MKzgk2.css +1 -0
- package/dist/app/_app/immutable/assets/DefaultTooltip.C5-uctZ7.css +1 -0
- package/dist/app/_app/immutable/assets/Group.DV48xipa.css +1 -0
- package/dist/app/_app/immutable/assets/Labels.BxZ4NUVz.css +1 -0
- package/dist/app/_app/immutable/assets/Legend.CxnrE4Ye.css +1 -0
- package/dist/app/_app/immutable/assets/Line.fkmsECm9.css +1 -0
- package/dist/app/_app/immutable/assets/Path.CvpwNZ6g.css +1 -0
- package/dist/app/_app/immutable/assets/Rect.CtRaGMmQ.css +1 -0
- package/dist/app/_app/immutable/assets/Text.j9l35qB0.css +1 -0
- package/dist/app/_app/immutable/assets/TransformContext.Bs_HkpAk.css +1 -0
- package/dist/app/_app/immutable/assets/Voronoi.ce7atosu.css +1 -0
- package/dist/app/_app/immutable/chunks/-aNGNaBT.js +1 -0
- package/dist/app/_app/immutable/chunks/6djn-yLs.js +1 -0
- package/dist/app/_app/immutable/chunks/B1amyutE.js +1 -0
- package/dist/app/_app/immutable/chunks/B3vZDoek.js +1 -0
- package/dist/app/_app/immutable/chunks/B5KRA4hC.js +1 -0
- package/dist/app/_app/immutable/chunks/BClnVG6H.js +1 -0
- package/dist/app/_app/immutable/chunks/BID1NNRh.js +1 -0
- package/dist/app/_app/immutable/chunks/BR2LaRms.js +1 -0
- package/dist/app/_app/immutable/chunks/Bd1gDe3Y.js +1 -0
- package/dist/app/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/dist/app/_app/immutable/chunks/Bl052uUt.js +1 -0
- package/dist/app/_app/immutable/chunks/Bye3lL0c.js +1 -0
- package/dist/app/_app/immutable/chunks/C58PZtCD.js +4 -0
- package/dist/app/_app/immutable/chunks/CAzydqEO.js +1 -0
- package/dist/app/_app/immutable/chunks/CCch3uox.js +1 -0
- package/dist/app/_app/immutable/chunks/CIlSMUH9.js +1 -0
- package/dist/app/_app/immutable/chunks/CO1vUXfR.js +1 -0
- package/dist/app/_app/immutable/chunks/CPbD8C65.js +5 -0
- package/dist/app/_app/immutable/chunks/CRTcXoMo.js +1 -0
- package/dist/app/_app/immutable/chunks/CjjyIQAO.js +1 -0
- package/dist/app/_app/immutable/chunks/CuXAxjvF.js +1 -0
- package/dist/app/_app/immutable/chunks/CvyVA_jC.js +1 -0
- package/dist/app/_app/immutable/chunks/CxGCFVdy.js +1 -0
- package/dist/app/_app/immutable/chunks/D0Ty6LN0.js +1 -0
- package/dist/app/_app/immutable/chunks/D2AaQUUW.js +1 -0
- package/dist/app/_app/immutable/chunks/D2BnX0Uk.js +3 -0
- package/dist/app/_app/immutable/chunks/DJc8C0NK.js +1 -0
- package/dist/app/_app/immutable/chunks/DKMlMI4a.js +1 -0
- package/dist/app/_app/immutable/chunks/DVXZkpbf.js +1 -0
- package/dist/app/_app/immutable/chunks/DVt8ukQ_.js +1 -0
- package/dist/app/_app/immutable/chunks/DZPlYdq_.js +1 -0
- package/dist/app/_app/immutable/chunks/Db0q5_zr.js +1 -0
- package/dist/app/_app/immutable/chunks/Dfvzj6n2.js +1 -0
- package/dist/app/_app/immutable/chunks/Dh958be7.js +1 -0
- package/dist/app/_app/immutable/chunks/DjKLLdnY.js +15 -0
- package/dist/app/_app/immutable/chunks/Doz7YX1W.js +1 -0
- package/dist/app/_app/immutable/chunks/DthYhn6Y.js +2 -0
- package/dist/app/_app/immutable/chunks/DtuTIrAM.js +1 -0
- package/dist/app/_app/immutable/chunks/HclGiUj8.js +1 -0
- package/dist/app/_app/immutable/chunks/Hx0TNsV3.js +1 -0
- package/dist/app/_app/immutable/chunks/RobXhXPM.js +1 -0
- package/dist/app/_app/immutable/chunks/V9ZjaxiY.js +1 -0
- package/dist/app/_app/immutable/chunks/Y5urAfNy.js +1 -0
- package/dist/app/_app/immutable/chunks/caXkbKD3.js +1 -0
- package/dist/app/_app/immutable/chunks/devYm2ud.js +1 -0
- package/dist/app/_app/immutable/chunks/mtZWP0zR.js +1 -0
- package/dist/app/_app/immutable/chunks/vDgBJUjM.js +1 -0
- package/dist/app/_app/immutable/chunks/xIq_fFFM.js +1 -0
- package/dist/app/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/app/_app/immutable/chunks/z05MoCFz.js +1 -0
- package/dist/app/_app/immutable/entry/app.CLAerUAN.js +2 -0
- package/dist/app/_app/immutable/entry/start.D3MqnNci.js +1 -0
- package/dist/app/_app/immutable/nodes/0.UTMEigHJ.js +1 -0
- package/dist/app/_app/immutable/nodes/1.Cn4f11bT.js +1 -0
- package/dist/app/_app/immutable/nodes/2.B39cIcr2.js +6 -0
- package/dist/app/_app/version.json +1 -0
- package/dist/app/index.html +82 -0
- package/dist/aws/clients.d.ts +70 -0
- package/dist/aws/clients.js +52 -0
- package/dist/aws/errors.d.ts +41 -0
- package/dist/aws/errors.js +70 -0
- package/dist/aws/firehose.d.ts +228 -0
- package/dist/aws/firehose.js +347 -0
- package/dist/aws/glue.d.ts +103 -0
- package/dist/aws/glue.js +225 -0
- package/dist/aws/lambda.d.ts +132 -0
- package/dist/aws/lambda.js +339 -0
- package/dist/aws/s3tables.d.ts +120 -0
- package/dist/aws/s3tables.js +281 -0
- package/dist/backfill.d.ts +100 -0
- package/dist/backfill.js +294 -0
- package/dist/commands.d.ts +124 -0
- package/dist/commands.js +336 -0
- package/dist/config.d.ts +162 -0
- package/dist/config.js +317 -0
- package/dist/fixture-ingest.d.ts +49 -0
- package/dist/fixture-ingest.js +43 -0
- package/dist/fixture-query.d.ts +39 -0
- package/dist/fixture-query.js +70 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/nodes.d.ts +404 -0
- package/dist/nodes.js +2708 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.js +47 -0
- package/dist/plugin.d.ts +102 -0
- package/dist/plugin.js +248 -0
- package/dist/ports.d.ts +113 -0
- package/dist/ports.js +35 -0
- package/dist/queries.d.ts +301 -0
- package/dist/queries.js +414 -0
- package/dist/schema.d.ts +240 -0
- package/dist/schema.js +154 -0
- package/dist/server.d.ts +150 -0
- package/dist/server.js +499 -0
- package/dist/transform/bots.d.ts +47 -0
- package/dist/transform/bots.js +73 -0
- package/dist/transform/handler.d.ts +135 -0
- package/dist/transform/handler.js +177 -0
- package/dist/transform/map-record.d.ts +110 -0
- package/dist/transform/map-record.js +275 -0
- package/dist/transform/visitor-key.d.ts +83 -0
- package/dist/transform/visitor-key.js +120 -0
- package/dist/transform-bundle/index.mjs +21456 -0
- package/dist/transform-bundle/transform-manifest.json +4 -0
- package/dist/transform-hash.d.ts +135 -0
- package/dist/transform-hash.js +186 -0
- package/dist/write-transform-manifest.mjs +365 -0
- package/package.json +59 -0
package/dist/nodes.d.ts
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The analytics plugin's resource graph. It owns the AWS resources the
|
|
3
|
+
* CloudFront-logs-to-Iceberg pipeline is built from and nothing else: the
|
|
4
|
+
* site's own bucket, distribution and log group stay in the CLI's graph
|
|
5
|
+
* (`packages/cli/src/nodes.ts`) and are never touched from here. This module
|
|
6
|
+
* carries all twelve of them, in four chains. The table chain - the S3 Tables bucket,
|
|
7
|
+
* the namespace inside it, the `page_views` table, and the Glue federation
|
|
8
|
+
* Firehose reads that table through - runs `analytics-table-bucket` ->
|
|
9
|
+
* `analytics-namespace` -> `analytics-table` ->
|
|
10
|
+
* `analytics-catalog-integration`. The transform chain - the long-lived
|
|
11
|
+
* `visitor_key` salt, the Lambda execution role whose policy names that
|
|
12
|
+
* secret's ARN, and the record-transform function itself - runs
|
|
13
|
+
* `analytics-salt-secret` -> `analytics-transform-role` ->
|
|
14
|
+
* `analytics-transform-function`. The delivery chain - the bucket every record
|
|
15
|
+
* Firehose cannot deliver lands in, the role it assumes, and the stream itself
|
|
16
|
+
* - runs `analytics-error-bucket` -> `analytics-firehose-role` ->
|
|
17
|
+
* `analytics-firehose-stream`, and joins the other two chains through the
|
|
18
|
+
* role's four grants and the stream's destination. The vended-delivery chain -
|
|
19
|
+
* the CloudWatch delivery destination pointing at that stream and the delivery
|
|
20
|
+
* joining it to the site's log source - runs `analytics-log-destination` ->
|
|
21
|
+
* `analytics-log-delivery` and hangs off the stream. All four are wired through
|
|
22
|
+
* `dependsOn`, and a node depends on every node whose recorded ARN it
|
|
23
|
+
* interpolates. {@link buildAnalyticsNodes} at the foot of this module returns
|
|
24
|
+
* the assembled set, and `plugin.ts` hands it to the SPI's `Plugin.nodes`;
|
|
25
|
+
* assembling an array is all it does - nothing here reconciles anything.
|
|
26
|
+
*
|
|
27
|
+
* **The delivery source the last chain hangs off is the site's, and this module
|
|
28
|
+
* only ever reads it.** AWS permits exactly one delivery source per
|
|
29
|
+
* distribution, so the site's CloudWatch delivery and this plugin's Firehose
|
|
30
|
+
* delivery necessarily share one, and `packages/cli/src/nodes.ts`'s
|
|
31
|
+
* `logDeliveryNode` owns it. Nothing here calls `putDeliverySource` or
|
|
32
|
+
* `deleteDeliverySource`; the source's name is read off `ctx.names` and the
|
|
33
|
+
* evidence that the site has been bootstrapped off `ctx.siteState`, the SPI's
|
|
34
|
+
* read-only view of the site's state.
|
|
35
|
+
*
|
|
36
|
+
* **Everything in this graph is created in `us-east-1`, whatever
|
|
37
|
+
* `config.region` says.** CloudFront standard logging accepts a Firehose
|
|
38
|
+
* delivery stream only in that region, so the whole pipeline - and therefore
|
|
39
|
+
* the table the stream writes into - has to live there too. The pin is
|
|
40
|
+
* enforced in exactly one place, `aws/clients.ts`, which builds every client
|
|
41
|
+
* over the host's `signingUsEast1` signer; no node here picks a region for a
|
|
42
|
+
* request. {@link ANALYTICS_REGION} below is the same region as *text*, needed
|
|
43
|
+
* only because an ARN spells its region out and because every node `title`
|
|
44
|
+
* states the pin, so the bootstrap output an operator reads carries it. Ten of
|
|
45
|
+
* the twelve titles state it as the region they are created in; the two IAM
|
|
46
|
+
* role nodes state it as the pipeline they serve, because IAM is global and
|
|
47
|
+
* "created in us-east-1" is not a property a role has (§Region pinning says so
|
|
48
|
+
* in as many words) - a title claiming otherwise would be the pin stated
|
|
49
|
+
* falsely rather than stated.
|
|
50
|
+
*
|
|
51
|
+
* The nodes are core's {@link ResourceNode} over {@link PluginContext}, so the
|
|
52
|
+
* CLI's own engine (`topoSort`/`applyGraph`/`destroyGraph`,
|
|
53
|
+
* `packages/cli/src/graph.ts`) reconciles them unchanged - this package
|
|
54
|
+
* contributes nodes, never a second engine.
|
|
55
|
+
*
|
|
56
|
+
* **Creating the namespace and the table through the S3 Tables control-plane
|
|
57
|
+
* API is a supported Firehose source.** Verified 2026-07-26 against AWS's S3
|
|
58
|
+
* Tables + Firehose walkthrough, which creates both with `aws s3tables
|
|
59
|
+
* create-namespace` and `aws s3tables create-table` and then points the
|
|
60
|
+
* delivery stream at them. This is worth recording because Firehose's
|
|
61
|
+
* considerations page carries a limitation that reads as if it forbids exactly
|
|
62
|
+
* that - "only tables created through Iceberg's GlueCatalog API". That
|
|
63
|
+
* limitation applies to plain Iceberg-on-S3 tables registered in Glue, not to
|
|
64
|
+
* S3 Tables reached through the `s3tablescatalog` federation
|
|
65
|
+
* (`analytics-catalog-integration`), which is how this pipeline reaches them.
|
|
66
|
+
* Without this note the next reader re-litigates it.
|
|
67
|
+
*/
|
|
68
|
+
import { type PluginContext, type ResourceNode } from 'blogwright-core';
|
|
69
|
+
import { type AnalyticsConfig } from './config.js';
|
|
70
|
+
/**
|
|
71
|
+
* The context every node in this module runs against: core's `PluginContext`
|
|
72
|
+
* narrowed to this plugin's own validated config block, which is what makes
|
|
73
|
+
* {@link resolveAnalyticsConfig} reachable from a node. `ResourceNode`'s own
|
|
74
|
+
* parameter is unconstrained (see core's `plugin.ts`), and the SPI's
|
|
75
|
+
* `Plugin.nodes` returns the bare `ResourceNode[]`; a node typed here is
|
|
76
|
+
* assignable to that, because `read`/`create`/`delete` are method-declared and
|
|
77
|
+
* therefore bivariant in their parameter type.
|
|
78
|
+
*/
|
|
79
|
+
type AnalyticsContext = PluginContext<AnalyticsConfig>;
|
|
80
|
+
/** One node of the analytics graph. */
|
|
81
|
+
type AnalyticsNode = ResourceNode<AnalyticsContext>;
|
|
82
|
+
/**
|
|
83
|
+
* The `analytics-firehose-stream` node id. Exported, alone among the twelve,
|
|
84
|
+
* because `analytics status` reads this node's recorded outputs back out of
|
|
85
|
+
* the scoped state its `read` hydrated - the stream's delivery health - and a
|
|
86
|
+
* second copy of the string in `commands.ts` would be a state key with two
|
|
87
|
+
* homes.
|
|
88
|
+
*/
|
|
89
|
+
export declare const FIREHOSE_STREAM_NODE = "analytics-firehose-stream";
|
|
90
|
+
/**
|
|
91
|
+
* The `analytics-log-delivery` node id. Exported for `backfill.ts`, which
|
|
92
|
+
* reads {@link CREATED_DAY_KEY} out of this node's recorded outputs: the
|
|
93
|
+
* backfill's idempotency bound and the node that writes it must name the same
|
|
94
|
+
* state entry, and a second spelling of the id is the one way that could stop
|
|
95
|
+
* being true without anything noticing.
|
|
96
|
+
*/
|
|
97
|
+
export declare const LOG_DELIVERY_NODE = "analytics-log-delivery";
|
|
98
|
+
/** The S3 Tables bucket every analytics table lives in. */
|
|
99
|
+
export declare function analyticsTableBucketNode(): AnalyticsNode;
|
|
100
|
+
/** The Iceberg namespace inside the table bucket. */
|
|
101
|
+
export declare function analyticsNamespaceNode(): AnalyticsNode;
|
|
102
|
+
/** The `page_views` table, carrying the schema and partition `schema.ts` owns. */
|
|
103
|
+
export declare function analyticsTableNode(): AnalyticsNode;
|
|
104
|
+
/**
|
|
105
|
+
* The Glue `s3tablescatalog` federation Firehose reads the `page_views` table
|
|
106
|
+
* through - **the one node in this graph that adopts shared state rather than
|
|
107
|
+
* owning it.**
|
|
108
|
+
*
|
|
109
|
+
* The integration is account-and-region scoped: a single catalog federates
|
|
110
|
+
* every S3 Tables bucket in the account and Region (see
|
|
111
|
+
* {@link federationSource}), so staging, production and anything else in the
|
|
112
|
+
* account that enabled the S3 Tables integration all read through the same one.
|
|
113
|
+
* Both halves of this node's behaviour follow from that. `read` adopts an
|
|
114
|
+
* existing federation instead of creating a second one, so a second environment
|
|
115
|
+
* converges on what is already there; `delete` removes nothing, so tearing one
|
|
116
|
+
* environment down leaves every other environment's pipeline intact.
|
|
117
|
+
*
|
|
118
|
+
* It depends on `analytics-table` rather than on the bucket even though the
|
|
119
|
+
* federation covers the account rather than any one table. Two reasons: the
|
|
120
|
+
* whole table chain is then in place before anything is federated, so
|
|
121
|
+
* `CreateCatalog`'s own `EntityNotFoundException` - which on that operation
|
|
122
|
+
* means the federated entity is missing, not the catalog - cannot fire merely
|
|
123
|
+
* because the bucket had not been created yet; and `destroyGraph` reverses the
|
|
124
|
+
* order, so this node's `delete` is reached first, before the table it was
|
|
125
|
+
* enabled for is removed.
|
|
126
|
+
*/
|
|
127
|
+
export declare function analyticsCatalogIntegrationNode(): AnalyticsNode;
|
|
128
|
+
/**
|
|
129
|
+
* The Secrets Manager secret holding the seed every `visitor_key` in the table
|
|
130
|
+
* is derived from - **the one resource in this graph that must outlive its own
|
|
131
|
+
* reconcile, and the one this module never overwrites or deletes.**
|
|
132
|
+
*
|
|
133
|
+
* `transform/visitor-key.ts` derives the per-day salt as
|
|
134
|
+
* `HMAC-SHA256(secret, day)` and `map-record.ts` hashes the viewer's address
|
|
135
|
+
* under it, so the stored value is the only thing standing between a row in
|
|
136
|
+
* `page_views` and the address it came from - the table keeps `user_agent` in
|
|
137
|
+
* the clear beside the key, and an unsalted SHA-256 over IPv4's 2^32 space is a
|
|
138
|
+
* lookup table, not a pseudonym. Two consequences shape every method below.
|
|
139
|
+
*
|
|
140
|
+
* **It is created once and never rewritten.** Replacing the value does not
|
|
141
|
+
* "rotate" anything: it orphans every `visitor_key` already written, because no
|
|
142
|
+
* row from before the change ever compares equal to a row from after it. The
|
|
143
|
+
* dashboard's unique-visitor figures would silently double at the boundary and
|
|
144
|
+
* `analytics backfill` - which re-derives a historical day's salt from this
|
|
145
|
+
* same seed - would produce rows that join to nothing. So `read` adopts, and
|
|
146
|
+
* `create` re-checks and adopts rather than trusting that the `read` before it
|
|
147
|
+
* is still true (see below). Daily turnover comes from the *derivation*, not
|
|
148
|
+
* from the store, which is why **no Secrets Manager rotation is configured**:
|
|
149
|
+
* managed rotation would mean a rotation Lambda, a schedule and a second
|
|
150
|
+
* execution role, to replace the one value that must not change.
|
|
151
|
+
*
|
|
152
|
+
* **Its `delete` removes nothing.** This is the second inert `delete` in the
|
|
153
|
+
* graph and it is inert for a different reason than
|
|
154
|
+
* `analytics-catalog-integration`'s, which is inert because the federation is
|
|
155
|
+
* shared. This one is inert because the act is asymmetric. Core's
|
|
156
|
+
* `deleteSecret` sends `ForceDeleteWithoutRecovery: true`, so there is no
|
|
157
|
+
* recovery window and nothing to undo with; keeping a secret costs cents a
|
|
158
|
+
* month and one command to remove by hand, while deleting one is unrecoverable
|
|
159
|
+
* and destroys the ability to interpret any `page_views` data that outlived the
|
|
160
|
+
* teardown - an exported copy, a table bucket whose own delete failed, or an
|
|
161
|
+
* environment torn down and re-bootstrapped, which is a routine recovery move
|
|
162
|
+
* and would otherwise come back with a different seed and no sign that
|
|
163
|
+
* anything had changed. The teardown says what it kept and how to remove it.
|
|
164
|
+
*/
|
|
165
|
+
export declare function analyticsSaltSecretNode(): AnalyticsNode;
|
|
166
|
+
/**
|
|
167
|
+
* The transform Lambda's execution role: permission to write its own logs and
|
|
168
|
+
* to read the one secret it needs, and nothing else.
|
|
169
|
+
*
|
|
170
|
+
* It declares `dependsOn: ['analytics-salt-secret']` because its policy
|
|
171
|
+
* interpolates that node's *recorded* ARN - the implementation notes' rule that
|
|
172
|
+
* "a node depends on every node whose recorded ARN it interpolates" - and
|
|
173
|
+
* {@link requireSaltSecretArn} explains what an undeclared edge would produce.
|
|
174
|
+
*/
|
|
175
|
+
export declare function analyticsTransformRoleNode(): AnalyticsNode;
|
|
176
|
+
/** What a reconcile of the deployed transform has to push. Both false is the common case. */
|
|
177
|
+
export interface TransformUpdate {
|
|
178
|
+
/** Send `UpdateFunctionCode` - the source hash moved. */
|
|
179
|
+
readonly code: boolean;
|
|
180
|
+
/** Send `UpdateFunctionConfiguration` - the settings the function runs under moved. */
|
|
181
|
+
readonly configuration: boolean;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Decide what an existing transform function's reconcile has to send, from what
|
|
185
|
+
* is recorded against what the package now holds. Pure, so the decision is
|
|
186
|
+
* testable without the AWS calls - `builderImageAction`'s shape
|
|
187
|
+
* (`packages/cli/src/nodes.ts:311-321`) and its reason.
|
|
188
|
+
*
|
|
189
|
+
* **Two independent comparisons, not one.** An unchanged hash performs no code
|
|
190
|
+
* call, which is the whole point of hashing the *source*: a rebuild on another
|
|
191
|
+
* machine emits different bundle bytes from the same source, and keying the
|
|
192
|
+
* decision on those bytes would redeploy the function on every platform switch
|
|
193
|
+
* (`transform-hash.ts` argues this at length). Everything a code push could
|
|
194
|
+
* need to accompany it is covered by that hash too, because `analytics/src` is
|
|
195
|
+
* one of the hash's inputs - so the module constants above cannot change
|
|
196
|
+
* without moving it.
|
|
197
|
+
*
|
|
198
|
+
* The configuration is compared separately because one of its inputs is *not*
|
|
199
|
+
* in the hash and cannot be: `analytics.saltSecretName` comes from
|
|
200
|
+
* `blogwright.config.json`, which nothing hashes. An operator who repoints it
|
|
201
|
+
* gets a new secret, a role granted on the new ARN, and - without this
|
|
202
|
+
* comparison - a function still reading the old name out of its environment:
|
|
203
|
+
* every batch failing `GetSecretValue`, every record in Firehose's error
|
|
204
|
+
* prefix. That is the same gap `builderImageAction` carries its second `logGroup`
|
|
205
|
+
* limb for.
|
|
206
|
+
*
|
|
207
|
+
* Keeping them separate is also what keeps the common cases to a single call
|
|
208
|
+
* each, which matters more than it looks: Lambda refuses a second update while
|
|
209
|
+
* the first is still settling, so two calls in one reconcile have a window the
|
|
210
|
+
* one-call cases do not.
|
|
211
|
+
*/
|
|
212
|
+
export declare function transformUpdate(recorded: {
|
|
213
|
+
sourceHash: string | undefined;
|
|
214
|
+
configuration: string | undefined;
|
|
215
|
+
}, desired: {
|
|
216
|
+
sourceHash: string;
|
|
217
|
+
configuration: string;
|
|
218
|
+
}): TransformUpdate;
|
|
219
|
+
/**
|
|
220
|
+
* The record-transform Lambda: the function Firehose runs over every CloudFront
|
|
221
|
+
* record before it reaches the `page_views` table.
|
|
222
|
+
*
|
|
223
|
+
* Its code is keyed by task 43's hash of the transform's **source**, recorded
|
|
224
|
+
* in the plugin's own state, so identical source never redeploys it - and the
|
|
225
|
+
* key that hash derives (`transformZipKey`) is recorded beside it as the
|
|
226
|
+
* artifact's name, even though the zip travels inline rather than through a
|
|
227
|
+
* bucket (see {@link MAX_INLINE_ZIP_BYTES} for why inline).
|
|
228
|
+
*
|
|
229
|
+
* It depends on `analytics-transform-role`, whose recorded ARN it runs as.
|
|
230
|
+
*/
|
|
231
|
+
export declare function analyticsTransformFunctionNode(): AnalyticsNode;
|
|
232
|
+
/**
|
|
233
|
+
* The S3 bucket every record Firehose cannot deliver is written to - **the
|
|
234
|
+
* physical place a silent pipeline failure becomes visible.**
|
|
235
|
+
*
|
|
236
|
+
* Firehose matches incoming JSON keys to the Iceberg column names exactly and
|
|
237
|
+
* *errors* the records that do not match to this bucket rather than dropping
|
|
238
|
+
* them (the change spec quotes the behaviour). So a missing column, a table
|
|
239
|
+
* whose catalog cannot be read, a Glue grant one level too narrow - none of
|
|
240
|
+
* them raise anything an operator sees. They fill this bucket while the
|
|
241
|
+
* dashboard stays empty, and this bucket is the only place the records
|
|
242
|
+
* themselves still exist. Two properties follow.
|
|
243
|
+
*
|
|
244
|
+
* **It is in {@link ANALYTICS_REGION}, with the rest of the pipeline**, created
|
|
245
|
+
* through the plugin's own {@link s3} client rather than `ctx.clients.s3`,
|
|
246
|
+
* which signs in `config.region`.
|
|
247
|
+
*
|
|
248
|
+
* **It is not the site's environment bucket.** The bucket the CLI's own
|
|
249
|
+
* `bucketNode` creates lives in
|
|
250
|
+
* `config.region` and `S3DestinationConfiguration.BucketARN` matches
|
|
251
|
+
* `arn:.*:s3:::[\w\.\-]{1,255}` - an S3 ARN carries no region, so the API can
|
|
252
|
+
* neither express a cross-region bucket nor reject one, and Firehose's
|
|
253
|
+
* cross-region documentation covers only HTTP endpoint destinations. Pointing
|
|
254
|
+
* at the site's bucket would therefore rest the pipeline's one recovery surface
|
|
255
|
+
* on undocumented behaviour, and would put failed-record objects - which carry
|
|
256
|
+
* the raw CloudFront fields, the viewer IP among them, precisely because the
|
|
257
|
+
* transform did not run on them - inside a bucket the site serves from.
|
|
258
|
+
*/
|
|
259
|
+
export declare function analyticsErrorBucketNode(): AnalyticsNode;
|
|
260
|
+
/**
|
|
261
|
+
* The role Firehose assumes to read the catalog, write the table, invoke the
|
|
262
|
+
* transform and store what it could not deliver - four grants, four concrete
|
|
263
|
+
* resources, no `*`.
|
|
264
|
+
*
|
|
265
|
+
* It declares `dependsOn` on the three nodes whose recorded ARNs those grants
|
|
266
|
+
* interpolate. `topoSort` drains zero-indegree nodes alphabetically
|
|
267
|
+
* (`packages/cli/src/graph.ts:35-38`), so a role declaring `dependsOn: []`
|
|
268
|
+
* would be reconciled *before* `analytics-transform-function` - `f` sorts before
|
|
269
|
+
* `t` - and the policy would interpolate an unrecorded output: a wrong
|
|
270
|
+
* permission written silently, never an error. `githubOidcRoleNode`
|
|
271
|
+
* (`packages/cli/src/nodes.ts:830`) is the precedent, declaring
|
|
272
|
+
* `cloudfront-distribution` for exactly this reason.
|
|
273
|
+
*/
|
|
274
|
+
export declare function analyticsFirehoseRoleNode(): AnalyticsNode;
|
|
275
|
+
/**
|
|
276
|
+
* The delivery stream itself: CloudFront's records in, the transform Lambda in
|
|
277
|
+
* front of the write, the `page_views` Iceberg table out, and the plugin's own
|
|
278
|
+
* error bucket for everything that does not make it.
|
|
279
|
+
*
|
|
280
|
+
* Its four edges are the ones its payload actually reads. The role edge is the
|
|
281
|
+
* spec's own rule - the `IcebergDestinationConfiguration` interpolates the
|
|
282
|
+
* role's recorded ARN - and it also carries `analytics-error-bucket`
|
|
283
|
+
* transitively, completing the `error-bucket -> firehose-role ->
|
|
284
|
+
* firehose-stream` chain. Without the role edge the ordering would survive only
|
|
285
|
+
* on `topoSort`'s alphabetical accident (`…-role` sorts before `…-stream`), the
|
|
286
|
+
* exact coincidence-reliance the spec's implementation notes warn against.
|
|
287
|
+
*
|
|
288
|
+
* **The `AppendOnly` reconcile is written against neither AWS document.** The
|
|
289
|
+
* Firehose considerations page says the flag is settable only with
|
|
290
|
+
* `CreateDeliveryStream`; the `IcebergDestinationUpdate` API reference lists it
|
|
291
|
+
* among the fields `UpdateDestination` accepts. They cannot both be right, and a
|
|
292
|
+
* node written against either alone is a defect whichever one turns out to be.
|
|
293
|
+
* So `update` attempts the in-place update first and falls back to replacing the
|
|
294
|
+
* stream when it is refused - and only when it is refused: the re-read that
|
|
295
|
+
* follows a successful update sits outside that `try`, because failing it is not
|
|
296
|
+
* a rejection and replacing a stream that was updated correctly would be pure
|
|
297
|
+
* loss. The order matters: `UpdateDestination` keeps the stream's ARN, while a
|
|
298
|
+
* replacement gets a new one - so the CloudFront log delivery task 53 builds
|
|
299
|
+
* would have to be repointed, and the records arriving during the gap are lost.
|
|
300
|
+
* Which path ran is in the log line.
|
|
301
|
+
*/
|
|
302
|
+
export declare function analyticsFirehoseStreamNode(): AnalyticsNode;
|
|
303
|
+
/**
|
|
304
|
+
* The state key holding the UTC day this plugin's delivery was **first**
|
|
305
|
+
* created - the idempotency bound the change spec's §Backfill of historical
|
|
306
|
+
* logs defines and task 61 reads.
|
|
307
|
+
*
|
|
308
|
+
* Written once and never advanced. Backfill inserts only whole days *strictly
|
|
309
|
+
* before* it, on the reasoning that Firehose received nothing before its
|
|
310
|
+
* delivery existed, so the two paths' row sets are disjoint. The two error
|
|
311
|
+
* directions are not symmetric, which is why the rule is write-once rather
|
|
312
|
+
* than keep-current: a bound that is too *early* loses at most the day at the
|
|
313
|
+
* seam, which the spec states and accepts, while a bound that moved *later*
|
|
314
|
+
* would let backfill insert days Firehose had already delivered and silently
|
|
315
|
+
* double every row in them. So a second reconcile, a re-created delivery and
|
|
316
|
+
* the destination node's Conflict retry all leave it exactly as it was.
|
|
317
|
+
*
|
|
318
|
+
* `read` never writes it either, even though it hydrates the rest of this
|
|
319
|
+
* node's outputs off the live delivery: `DescribeDeliveries` reports no
|
|
320
|
+
* creation date, so a delivery found already attached to a state file that
|
|
321
|
+
* lost this key leaves task 61 with no bound and an actionable refusal. That
|
|
322
|
+
* is the loud direction, and it is preferred to today's date, which would be a
|
|
323
|
+
* bound that moved later.
|
|
324
|
+
*
|
|
325
|
+
* Exported since task 61, which reads it. It was deliberately module-private
|
|
326
|
+
* while nothing consumed it - an exported constant with no consumer is what
|
|
327
|
+
* `pnpm knip` catches - but a private constant restated in its reader is worse
|
|
328
|
+
* than an exported one: the two spellings would have to agree and nothing
|
|
329
|
+
* would check that they did.
|
|
330
|
+
*/
|
|
331
|
+
export declare const CREATED_DAY_KEY = "createdDay";
|
|
332
|
+
/**
|
|
333
|
+
* The CloudWatch delivery destination the site's CloudFront records are
|
|
334
|
+
* delivered to - **this plugin's own, alongside the site's and never in place
|
|
335
|
+
* of it.**
|
|
336
|
+
*
|
|
337
|
+
* Its one edge is the node whose recorded ARN it points at. `PutDeliveryDestination`
|
|
338
|
+
* accepts a `destinationResourceArn` for a resource that does not exist yet, so
|
|
339
|
+
* without the edge the destination would be created against `undefined` and the
|
|
340
|
+
* first symptom would be records going nowhere - see {@link requireStreamArn}.
|
|
341
|
+
*
|
|
342
|
+
* The output format is the one thing about a destination that cannot be
|
|
343
|
+
* changed once it exists, which is why `update` replaces rather than mutates
|
|
344
|
+
* and why the configured format is recorded beside the ARN in the first place.
|
|
345
|
+
*/
|
|
346
|
+
export declare function analyticsLogDestinationNode(): AnalyticsNode;
|
|
347
|
+
/**
|
|
348
|
+
* The delivery joining the site's existing delivery source to this plugin's
|
|
349
|
+
* destination - **a second delivery on a source the plugin reads, never
|
|
350
|
+
* creates, never repoints and never deletes.**
|
|
351
|
+
*
|
|
352
|
+
* `putDeliverySource` is not called here and must never be. AWS permits one
|
|
353
|
+
* delivery source per distribution and the site's node owns it
|
|
354
|
+
* (`packages/cli/src/nodes.ts`'s `logDeliveryNode`); this node reads its name
|
|
355
|
+
* off `ctx.names` and attaches a second delivery beside the site's CloudWatch
|
|
356
|
+
* one. The site's copy is left with the field list AWS defaults to, which is
|
|
357
|
+
* deliberate and is `schema.ts`'s to explain.
|
|
358
|
+
*
|
|
359
|
+
* There is no `update`. CloudWatch Logs has no `UpdateDelivery`: the record
|
|
360
|
+
* fields a delivery selects are fixed when it is created, exactly as the
|
|
361
|
+
* `page_views` table's schema is fixed when *it* is created
|
|
362
|
+
* (`aws/s3tables.ts`'s `createTable` reconciles no existing schema). Changing
|
|
363
|
+
* the column set is a rebuild of this pipeline in both places, not a
|
|
364
|
+
* reconcile, and pretending otherwise in one of the two would be worse than
|
|
365
|
+
* saying so in both.
|
|
366
|
+
*/
|
|
367
|
+
export declare function analyticsLogDeliveryNode(): AnalyticsNode;
|
|
368
|
+
/**
|
|
369
|
+
* The plugin's twelve resource nodes, assembled in the order the change spec's
|
|
370
|
+
* §Analytics pipeline → Resource nodes table lists them. This is what
|
|
371
|
+
* `Plugin.nodes` (`plugin.ts`) hands the CLI's generic `analytics bootstrap`
|
|
372
|
+
* and `analytics destroy` verbs, and it is the whole of what this package
|
|
373
|
+
* contributes to a reconcile: the engine that walks them - `topoSort`,
|
|
374
|
+
* `applyGraph`, `destroyGraph` - is the CLI's own and is never reimplemented
|
|
375
|
+
* here.
|
|
376
|
+
*
|
|
377
|
+
* **The returned order is itself a topological order**, and that is a property
|
|
378
|
+
* of this array rather than a coincidence of the table's layout: every node's
|
|
379
|
+
* `dependsOn` names only nodes that appear EARLIER in it. That is worth stating
|
|
380
|
+
* because it is exactly the witness `topoSort`'s two failure modes are the
|
|
381
|
+
* absence of - a dependency naming a node outside the set, and a cycle - so a
|
|
382
|
+
* test that checks it has proved the set passes `topoSort` without running a
|
|
383
|
+
* second copy of `topoSort` to find out. `applyGraph` sorts the array again
|
|
384
|
+
* regardless and does not rely on the order it arrives in; nothing here may
|
|
385
|
+
* assume the reconcile follows this sequence, only that this sequence is a
|
|
386
|
+
* legal one.
|
|
387
|
+
*
|
|
388
|
+
* **No `ctx` parameter, deliberately.** The SPI declares `nodes?(ctx)` and the
|
|
389
|
+
* CLI calls it with one, so this function is assignable to it as written - a
|
|
390
|
+
* zero-argument function satisfies a one-argument signature. None of the twelve
|
|
391
|
+
* factories needs a context to be *built*: each reads `ctx` inside `read`,
|
|
392
|
+
* `create`, `update` and `delete`, when the reconcile is actually running. A
|
|
393
|
+
* parameter accepted and ignored here would be an unused binding and, worse, a
|
|
394
|
+
* claim that the SET varies with the context - it does not, and `analytics
|
|
395
|
+
* status` and `analytics destroy` both depend on it not doing so. (The plan's
|
|
396
|
+
* task 54 spells this function `buildAnalyticsNodes(ctx)`; the argument is what
|
|
397
|
+
* changed, not the wiring.)
|
|
398
|
+
*
|
|
399
|
+
* A fresh array of fresh nodes on every call, matching `buildNodes`
|
|
400
|
+
* (`packages/cli/src/nodes.ts`): a node object carries no state between
|
|
401
|
+
* reconciles, and two calls in one process must not share one.
|
|
402
|
+
*/
|
|
403
|
+
export declare function buildAnalyticsNodes(): AnalyticsNode[];
|
|
404
|
+
export {};
|