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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bodies behind the three actions `plugin.ts` declares, and **the
|
|
3
|
+
* plugin's composition root**. The command TABLE is written once, in
|
|
4
|
+
* `plugin.ts`, and no later task edits it; each action's behaviour lands here
|
|
5
|
+
* instead - `status` and `dashboard` below in full, and `backfill` as the
|
|
6
|
+
* three lines that construct its two adapters, its logic living in
|
|
7
|
+
* `backfill.ts`. All three are dispatchable from the moment the manifest field
|
|
8
|
+
* makes this package discoverable.
|
|
9
|
+
*
|
|
10
|
+
* A body takes exactly the parameters it needs. `PluginCommand.run(ctx, args)`
|
|
11
|
+
* accepts a narrower function - a zero-argument function is assignable to it,
|
|
12
|
+
* and so is one taking a `Pick` of `PluginContext` - so filling a body in
|
|
13
|
+
* never changes the table in `plugin.ts`.
|
|
14
|
+
*
|
|
15
|
+
* **Composition root** means one thing concretely: this is the only module in
|
|
16
|
+
* the package that constructs a DuckDB adapter and the only one that resolves
|
|
17
|
+
* credentials, exactly as `packages/cli/src/context.ts` is for the CLI's own
|
|
18
|
+
* adapters. Every module below it - the server, the named query set, the data
|
|
19
|
+
* shaping, the backfill - is handed a *port* and can name no vendor at all.
|
|
20
|
+
* All three commands that reach the table construct their adapters on their
|
|
21
|
+
* own line here: `dashboard` hands one to the server it starts, and `status`
|
|
22
|
+
* and `backfill` take theirs as defaulted parameters, which is what lets a
|
|
23
|
+
* test drive the whole command over a fake without patching a module.
|
|
24
|
+
*
|
|
25
|
+
* `backfill` is the only one that constructs two of them, and they are not
|
|
26
|
+
* interchangeable: its occupancy count crosses `AnalyticsQuery`, whose attach
|
|
27
|
+
* is read-only, and its insert crosses `AnalyticsIngest`, whose attach is not.
|
|
28
|
+
* One writable session answering both would be shorter and would put the
|
|
29
|
+
* dashboard's own read path one refactor away from a connection that can
|
|
30
|
+
* write.
|
|
31
|
+
*
|
|
32
|
+
* `bootstrap` and `destroy` are deliberately absent from this module as well
|
|
33
|
+
* as from the table: they are always the CLI's generic lifecycle verbs, run
|
|
34
|
+
* by an engine a plugin may not import - see `plugin.ts`'s own comment.
|
|
35
|
+
*/
|
|
36
|
+
import { type PluginContext } from 'blogwright-core';
|
|
37
|
+
import { type BackfillPorts } from './backfill.js';
|
|
38
|
+
import { type AnalyticsConfig } from './config.js';
|
|
39
|
+
import type { AnalyticsQuery } from './ports.js';
|
|
40
|
+
/**
|
|
41
|
+
* `analytics status`: the plugin's own nodes read against
|
|
42
|
+
* `state/<env>.analytics.json`, plus the Firehose stream's delivery health
|
|
43
|
+
* and the table's current row count. Declared rather than left to the
|
|
44
|
+
* generic `status` verb because it does strictly more than that verb does
|
|
45
|
+
* (§Analytics plugin → Namespace and commands), which task 16's precedence
|
|
46
|
+
* permits: only `bootstrap` and `destroy` are reserved.
|
|
47
|
+
*
|
|
48
|
+
* **It takes the whole `PluginContext`, not a `Pick` of it.** Every other
|
|
49
|
+
* consumer in this package narrows (`DashboardCommandContext`,
|
|
50
|
+
* `DuckDbSessionContext`, `AnalyticsConfigContext`), and this one cannot: it
|
|
51
|
+
* hands `ctx` to `read()` on each of the plugin's own nodes, and a node is a
|
|
52
|
+
* `ResourceNode<PluginContext<AnalyticsConfig>>`, so the narrowest type that
|
|
53
|
+
* type-checks is the SPI context entire. A `Pick` listing its fifteen required members
|
|
54
|
+
* would be that type with a second name.
|
|
55
|
+
*
|
|
56
|
+
* `args` is accepted and unused: the host calls `run(ctx, args)` for every
|
|
57
|
+
* declared command, and this one takes no arguments of its own - the
|
|
58
|
+
* environment is a positional the dispatcher has already consumed, and
|
|
59
|
+
* `--plain` reaches this command as `ctx.ports.terminal.isInteractive` rather
|
|
60
|
+
* than as a flag to parse.
|
|
61
|
+
*
|
|
62
|
+
* `query` is a defaulted parameter for the reason the site's own
|
|
63
|
+
* `status(ctx, nodes = buildNodes(ctx))` takes its node set that way: every
|
|
64
|
+
* real call site passes nothing and gets the composition root's adapter, and a
|
|
65
|
+
* test hands it the fixture-backed fake instead of patching a module. The
|
|
66
|
+
* default constructs the adapter but does not connect - the connection and the
|
|
67
|
+
* credentials are resolved inside the first query - so a status that never
|
|
68
|
+
* reaches the table starts nothing.
|
|
69
|
+
*/
|
|
70
|
+
export declare function status(ctx: PluginContext<AnalyticsConfig>, _args?: string[], query?: AnalyticsQuery): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* The slice of a plugin context {@link dashboard} reads, taken as a `Pick` of
|
|
73
|
+
* core's own `PluginContext` rather than a restatement of it, the way
|
|
74
|
+
* `DuckDbSessionContext` and `AnalyticsConfigContext` already are: the members
|
|
75
|
+
* cannot drift from the SPI, any `PluginContext<AnalyticsConfig>` satisfies
|
|
76
|
+
* it, and a test builds the six it needs instead of the SPI's sixteen.
|
|
77
|
+
*/
|
|
78
|
+
export type DashboardCommandContext = Pick<PluginContext<AnalyticsConfig>, 'env' | 'config' | 'pluginConfig' | 'accountId' | 'ports' | 'logger'>;
|
|
79
|
+
/**
|
|
80
|
+
* `analytics dashboard`: serve the local dashboard over the Iceberg table,
|
|
81
|
+
* bound to `127.0.0.1` on the resolved `dashboard.port`.
|
|
82
|
+
*
|
|
83
|
+
* This is the composition root the module comment describes, and the three
|
|
84
|
+
* lines that make it one are the adapter construction, the credential
|
|
85
|
+
* resolution and the `appDir`. **Credentials are core's own provider chain**
|
|
86
|
+
* (`createCredentialProvider`, `packages/core/src/aws/credentials.ts`),
|
|
87
|
+
* resolved here and handed to the adapter, which is the spec's §Analytics
|
|
88
|
+
* dashboard → Credentials: one credential source serves the whole CLI, so a
|
|
89
|
+
* session that works for `deploy` works for the dashboard. It is built with no
|
|
90
|
+
* `override`, matching `transform/entry.ts`: that flag substitutes an
|
|
91
|
+
* emulator's dummy `test`/`test` pair when a real chain fails, and the
|
|
92
|
+
* dashboard has no emulator to talk to - the adapter attaches a real S3 Tables
|
|
93
|
+
* ARN with no endpoint override at all, so dummy credentials would turn "you
|
|
94
|
+
* are not logged in" into an opaque authorisation failure from AWS.
|
|
95
|
+
*
|
|
96
|
+
* Constructing the adapter touches neither the network nor the native library
|
|
97
|
+
* - it opens its connection lazily on the first query - so the listener is
|
|
98
|
+
* bound and the URL is printed before AWS is ever consulted.
|
|
99
|
+
*
|
|
100
|
+
* The `finally` is the shutdown path, and it covers both ways out: the signal
|
|
101
|
+
* {@link untilStopped} waits for, and a failure raised while waiting. Either
|
|
102
|
+
* way `close()` is awaited, so the port is released before this function
|
|
103
|
+
* returns and the next `analytics dashboard` binds it again.
|
|
104
|
+
*/
|
|
105
|
+
export declare function dashboard(ctx: DashboardCommandContext): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* `analytics backfill`: the optional, one-shot, idempotent pull of history
|
|
108
|
+
* that predates the Firehose delivery, from the site's CloudWatch log group
|
|
109
|
+
* into the table. Never part of the steady-state pipeline. What it does is
|
|
110
|
+
* `backfill.ts`'s; what this line owns is the two adapters it does it through.
|
|
111
|
+
*
|
|
112
|
+
* `ports` is a defaulted parameter for the reason `status`'s `query` is: every
|
|
113
|
+
* real call site passes nothing and gets the composition root's adapters, and
|
|
114
|
+
* a test hands it a recording pair instead of patching a module. Constructing
|
|
115
|
+
* either adapter touches neither the network nor the native library - both
|
|
116
|
+
* open their connection inside the first statement - so the command's refusal
|
|
117
|
+
* when the plugin's state carries no delivery bound still happens before
|
|
118
|
+
* anything is opened.
|
|
119
|
+
*
|
|
120
|
+
* `args` is accepted and unused: the host calls `run(ctx, args)` for every
|
|
121
|
+
* declared command and this one takes no arguments of its own - the
|
|
122
|
+
* environment is a positional the dispatcher has already consumed.
|
|
123
|
+
*/
|
|
124
|
+
export declare function backfill(ctx: PluginContext<AnalyticsConfig>, _args?: string[], ports?: BackfillPorts): Promise<void>;
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bodies behind the three actions `plugin.ts` declares, and **the
|
|
3
|
+
* plugin's composition root**. The command TABLE is written once, in
|
|
4
|
+
* `plugin.ts`, and no later task edits it; each action's behaviour lands here
|
|
5
|
+
* instead - `status` and `dashboard` below in full, and `backfill` as the
|
|
6
|
+
* three lines that construct its two adapters, its logic living in
|
|
7
|
+
* `backfill.ts`. All three are dispatchable from the moment the manifest field
|
|
8
|
+
* makes this package discoverable.
|
|
9
|
+
*
|
|
10
|
+
* A body takes exactly the parameters it needs. `PluginCommand.run(ctx, args)`
|
|
11
|
+
* accepts a narrower function - a zero-argument function is assignable to it,
|
|
12
|
+
* and so is one taking a `Pick` of `PluginContext` - so filling a body in
|
|
13
|
+
* never changes the table in `plugin.ts`.
|
|
14
|
+
*
|
|
15
|
+
* **Composition root** means one thing concretely: this is the only module in
|
|
16
|
+
* the package that constructs a DuckDB adapter and the only one that resolves
|
|
17
|
+
* credentials, exactly as `packages/cli/src/context.ts` is for the CLI's own
|
|
18
|
+
* adapters. Every module below it - the server, the named query set, the data
|
|
19
|
+
* shaping, the backfill - is handed a *port* and can name no vendor at all.
|
|
20
|
+
* All three commands that reach the table construct their adapters on their
|
|
21
|
+
* own line here: `dashboard` hands one to the server it starts, and `status`
|
|
22
|
+
* and `backfill` take theirs as defaulted parameters, which is what lets a
|
|
23
|
+
* test drive the whole command over a fake without patching a module.
|
|
24
|
+
*
|
|
25
|
+
* `backfill` is the only one that constructs two of them, and they are not
|
|
26
|
+
* interchangeable: its occupancy count crosses `AnalyticsQuery`, whose attach
|
|
27
|
+
* is read-only, and its insert crosses `AnalyticsIngest`, whose attach is not.
|
|
28
|
+
* One writable session answering both would be shorter and would put the
|
|
29
|
+
* dashboard's own read path one refactor away from a connection that can
|
|
30
|
+
* write.
|
|
31
|
+
*
|
|
32
|
+
* `bootstrap` and `destroy` are deliberately absent from this module as well
|
|
33
|
+
* as from the table: they are always the CLI's generic lifecycle verbs, run
|
|
34
|
+
* by an engine a plugin may not import - see `plugin.ts`'s own comment.
|
|
35
|
+
*/
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
import { colors, createCredentialProvider } from 'blogwright-core';
|
|
38
|
+
import { createDuckDbAnalyticsIngest } from './adapters/duckdb-ingest.js';
|
|
39
|
+
import { createDuckDbAnalyticsQuery } from './adapters/duckdb-query.js';
|
|
40
|
+
import { runBackfill } from './backfill.js';
|
|
41
|
+
import { resolveAnalyticsConfig } from './config.js';
|
|
42
|
+
import { buildAnalyticsNodes, FIREHOSE_STREAM_NODE } from './nodes.js';
|
|
43
|
+
import { ROW_COUNT_COLUMN, ROW_COUNT_QUERY, WHOLE_TABLE_RANGE } from './queries.js';
|
|
44
|
+
import { createDashboardServer } from './server.js';
|
|
45
|
+
/**
|
|
46
|
+
* The signals that stop a foreground command. `SIGTERM` joins `SIGINT`
|
|
47
|
+
* because a dashboard is as likely to be stopped by a supervisor or a
|
|
48
|
+
* container runtime as by an operator's Ctrl+C, and both must release the
|
|
49
|
+
* listener rather than leave the port held by a half-dead process.
|
|
50
|
+
*/
|
|
51
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
52
|
+
/** The tree glyphs the pretty form marks each state with - the CLI's own. */
|
|
53
|
+
const STATUS_MARKS = {
|
|
54
|
+
present: colors.green('✓'),
|
|
55
|
+
missing: colors.yellow('◌'),
|
|
56
|
+
error: colors.red('✗'),
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The one {@link DeliveryState} that means records are reaching the table.
|
|
60
|
+
* Typed against the client's own union rather than spelled as a bare string,
|
|
61
|
+
* so renaming a state there is a compile error here instead of a status line
|
|
62
|
+
* that silently never reports healthy again.
|
|
63
|
+
*/
|
|
64
|
+
const HEALTHY_DELIVERY_STATE = 'active';
|
|
65
|
+
/**
|
|
66
|
+
* Read every node the plugin contributes against the live account, in the
|
|
67
|
+
* order {@link buildAnalyticsNodes} returns them (no topological sort - a
|
|
68
|
+
* status is a read, not a reconcile).
|
|
69
|
+
*
|
|
70
|
+
* A `read()` that throws becomes an `error` entry rather than ending the
|
|
71
|
+
* listing: one unreadable item must not take down the whole listing, which is
|
|
72
|
+
* `history`'s manifest loop in `packages/cli/src/commands.ts` and, for exactly
|
|
73
|
+
* this shape, the CLI's own `readNodeStatus`. Nothing is saved - `read()`
|
|
74
|
+
* hydrates `ctx.state` in memory and this command never calls `ctx.save()`, so
|
|
75
|
+
* a status can neither create nor rewrite `state/<env>.analytics.json`.
|
|
76
|
+
*/
|
|
77
|
+
async function readNodeEntries(ctx) {
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const node of buildAnalyticsNodes()) {
|
|
80
|
+
try {
|
|
81
|
+
const exists = await node.read(ctx);
|
|
82
|
+
entries.push({ id: node.id, title: node.title, state: exists ? 'present' : 'missing' });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
entries.push({
|
|
86
|
+
id: node.id,
|
|
87
|
+
title: node.title,
|
|
88
|
+
state: 'error',
|
|
89
|
+
detail: err.message,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Report the node listing: the drift tree on an interactive terminal, one
|
|
97
|
+
* stable line per node otherwise. The same split, the same marks and the same
|
|
98
|
+
* `read failed` wording as the site's own `status`
|
|
99
|
+
* (`packages/cli/src/commands.ts`, through `logStatusEntries` in its
|
|
100
|
+
* `render.ts`), restated here because a plugin may not import that module.
|
|
101
|
+
*
|
|
102
|
+
* One deliberate difference in the plain form: the CLI appends
|
|
103
|
+
* `JSON.stringify` of the node's recorded outputs to each line. This command
|
|
104
|
+
* does not. The plain form is the contract CI and agents read, and a line
|
|
105
|
+
* carrying an ARN carries the account id, the environment and a
|
|
106
|
+
* service-generated table id with it - so the "same" line would differ between
|
|
107
|
+
* two environments of the same site and could never be asserted as a contract.
|
|
108
|
+
* What the outputs say is in `state/<env>.analytics.json`, and the two lines
|
|
109
|
+
* this command adds after the listing are what a reader wants them for.
|
|
110
|
+
*/
|
|
111
|
+
function logNodeEntries(entries, pretty, logger) {
|
|
112
|
+
if (pretty) {
|
|
113
|
+
entries.forEach((entry, index) => {
|
|
114
|
+
const connector = index === entries.length - 1 ? '╰─' : '├─';
|
|
115
|
+
const detail = entry.detail === undefined ? '' : ` ${colors.dim(entry.detail)}`;
|
|
116
|
+
logger.info(`${connector} ${STATUS_MARKS[entry.state]} ${entry.title}${detail}`);
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// The plain form is the stable contract for CI logs and agents.
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (entry.state === 'error') {
|
|
123
|
+
logger.warn(`${entry.title}: read failed (${entry.detail})`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const mark = entry.state === 'present' ? colors.green('present') : colors.yellow('missing');
|
|
127
|
+
logger.info(` ${mark} ${entry.title}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Report the delivery stream's health from the state its own `read` hydrated a
|
|
132
|
+
* moment ago (task 51's `recordStream` puts the stream's `state` and, when the
|
|
133
|
+
* service reports one, its `failure` into the plugin's scoped state). No
|
|
134
|
+
* second describe is issued: the node has just made that call, and a status
|
|
135
|
+
* that made it twice would report two different answers on a stream that
|
|
136
|
+
* changed in between.
|
|
137
|
+
*
|
|
138
|
+
* A stream that is absent is reported as a warning rather than as health, and
|
|
139
|
+
* that choice is worth stating because the definition of done does not pin it:
|
|
140
|
+
* "no stream" and "a stream in `create-failed`" are the same operational fact
|
|
141
|
+
* - nothing is being delivered - and the listing above already says which node
|
|
142
|
+
* is missing, so a health line that stayed silent would read as healthy.
|
|
143
|
+
*/
|
|
144
|
+
function logStreamHealth(ctx, entries) {
|
|
145
|
+
const entry = entries.find((candidate) => candidate.id === FIREHOSE_STREAM_NODE);
|
|
146
|
+
if (entry?.state === 'error') {
|
|
147
|
+
ctx.logger.warn(`Firehose delivery: unavailable - reading the stream failed (${entry.detail})`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (entry?.state !== 'present') {
|
|
151
|
+
ctx.logger.warn(`Firehose delivery: no delivery stream - \`blogwright analytics bootstrap ${ctx.env}\` creates it`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const recorded = ctx.state.resources[FIREHOSE_STREAM_NODE];
|
|
155
|
+
const state = typeof recorded?.['state'] === 'string' ? recorded['state'] : 'unrecorded';
|
|
156
|
+
const failure = typeof recorded?.['failure'] === 'string' ? ` - ${recorded['failure']}` : '';
|
|
157
|
+
if (state === HEALTHY_DELIVERY_STATE) {
|
|
158
|
+
ctx.logger.info(` Firehose delivery: ${state}`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
ctx.logger.warn(`Firehose delivery: ${state}${failure}`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Report the table's current row count, taken through the `AnalyticsQuery`
|
|
165
|
+
* port by name. The command writes no SQL: `ROW_COUNT_QUERY` names one of the
|
|
166
|
+
* definitions in `queries.ts`, and {@link WHOLE_TABLE_RANGE} is what "current
|
|
167
|
+
* row count" means for a set whose every definition is bounded on the `day`
|
|
168
|
+
* partition. `includeBots` is bound explicitly rather than left to
|
|
169
|
+
* `config.analytics.bots`, because this is the table's row count and not a
|
|
170
|
+
* dashboard figure - a bot row is still a row.
|
|
171
|
+
*
|
|
172
|
+
* A failed read degrades to a warning, which is the whole point of doing it
|
|
173
|
+
* last: the table is the one part of this listing that needs credentials the
|
|
174
|
+
* node reads do not (the vendor library attaches the catalog itself), so an
|
|
175
|
+
* operator with no session still gets all twelve nodes and the stream's health.
|
|
176
|
+
*/
|
|
177
|
+
async function logRowCount(ctx, query) {
|
|
178
|
+
const config = resolveAnalyticsConfig(ctx);
|
|
179
|
+
const relation = `${config.namespace}.${config.table}`;
|
|
180
|
+
try {
|
|
181
|
+
const rows = await query.run(ROW_COUNT_QUERY, {
|
|
182
|
+
range: WHOLE_TABLE_RANGE,
|
|
183
|
+
includeBots: true,
|
|
184
|
+
});
|
|
185
|
+
const count = rows[0]?.[ROW_COUNT_COLUMN];
|
|
186
|
+
if (typeof count !== 'number') {
|
|
187
|
+
throw new Error(`the ${ROW_COUNT_QUERY} query answered no ${ROW_COUNT_COLUMN}`);
|
|
188
|
+
}
|
|
189
|
+
ctx.logger.info(` rows in ${relation}: ${count}`);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
ctx.logger.warn(`rows in ${relation}: unavailable - ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* `analytics status`: the plugin's own nodes read against
|
|
197
|
+
* `state/<env>.analytics.json`, plus the Firehose stream's delivery health
|
|
198
|
+
* and the table's current row count. Declared rather than left to the
|
|
199
|
+
* generic `status` verb because it does strictly more than that verb does
|
|
200
|
+
* (§Analytics plugin → Namespace and commands), which task 16's precedence
|
|
201
|
+
* permits: only `bootstrap` and `destroy` are reserved.
|
|
202
|
+
*
|
|
203
|
+
* **It takes the whole `PluginContext`, not a `Pick` of it.** Every other
|
|
204
|
+
* consumer in this package narrows (`DashboardCommandContext`,
|
|
205
|
+
* `DuckDbSessionContext`, `AnalyticsConfigContext`), and this one cannot: it
|
|
206
|
+
* hands `ctx` to `read()` on each of the plugin's own nodes, and a node is a
|
|
207
|
+
* `ResourceNode<PluginContext<AnalyticsConfig>>`, so the narrowest type that
|
|
208
|
+
* type-checks is the SPI context entire. A `Pick` listing its fifteen required members
|
|
209
|
+
* would be that type with a second name.
|
|
210
|
+
*
|
|
211
|
+
* `args` is accepted and unused: the host calls `run(ctx, args)` for every
|
|
212
|
+
* declared command, and this one takes no arguments of its own - the
|
|
213
|
+
* environment is a positional the dispatcher has already consumed, and
|
|
214
|
+
* `--plain` reaches this command as `ctx.ports.terminal.isInteractive` rather
|
|
215
|
+
* than as a flag to parse.
|
|
216
|
+
*
|
|
217
|
+
* `query` is a defaulted parameter for the reason the site's own
|
|
218
|
+
* `status(ctx, nodes = buildNodes(ctx))` takes its node set that way: every
|
|
219
|
+
* real call site passes nothing and gets the composition root's adapter, and a
|
|
220
|
+
* test hands it the fixture-backed fake instead of patching a module. The
|
|
221
|
+
* default constructs the adapter but does not connect - the connection and the
|
|
222
|
+
* credentials are resolved inside the first query - so a status that never
|
|
223
|
+
* reaches the table starts nothing.
|
|
224
|
+
*/
|
|
225
|
+
export async function status(ctx, _args = [], query = createDuckDbAnalyticsQuery({
|
|
226
|
+
ctx,
|
|
227
|
+
credentials: createCredentialProvider({}),
|
|
228
|
+
})) {
|
|
229
|
+
ctx.logger.info(colors.bold(`Analytics status for "${ctx.env}" (bucket ${ctx.names.bucket})`));
|
|
230
|
+
const entries = await readNodeEntries(ctx);
|
|
231
|
+
logNodeEntries(entries, ctx.ports.terminal.isInteractive, ctx.logger);
|
|
232
|
+
logStreamHealth(ctx, entries);
|
|
233
|
+
await logRowCount(ctx, query);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The prebuilt dashboard application task 57 emits, beside this module's own
|
|
237
|
+
* compiled output (`dist/commands.js` → `dist/app`). Located from
|
|
238
|
+
* `import.meta.url` for the reason `cliPackageDir` (`packages/cli/src/context.ts`)
|
|
239
|
+
* is: a package's own files are not reachable through its `exports` map, so
|
|
240
|
+
* self-location is a composition-root concern and not something the server -
|
|
241
|
+
* which is handed the resolved directory as data - can derive. Under the test
|
|
242
|
+
* runner it resolves beside the sources instead, where no application is
|
|
243
|
+
* built, and the server answers a 503 naming the directory.
|
|
244
|
+
*/
|
|
245
|
+
function dashboardAppDir() {
|
|
246
|
+
return fileURLToPath(new URL('app', import.meta.url));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolve once a stop signal arrives, un-registering both listeners first so
|
|
250
|
+
* a finished command leaves the process exactly as it found it. Resolves with
|
|
251
|
+
* the signal, so the line the operator sees says which one stopped it.
|
|
252
|
+
*/
|
|
253
|
+
function untilStopped() {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
const listeners = new Map();
|
|
256
|
+
const stop = (signal) => {
|
|
257
|
+
for (const [name, listener] of listeners)
|
|
258
|
+
process.off(name, listener);
|
|
259
|
+
resolve(signal);
|
|
260
|
+
};
|
|
261
|
+
for (const signal of STOP_SIGNALS) {
|
|
262
|
+
const listener = () => stop(signal);
|
|
263
|
+
listeners.set(signal, listener);
|
|
264
|
+
process.on(signal, listener);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* `analytics dashboard`: serve the local dashboard over the Iceberg table,
|
|
270
|
+
* bound to `127.0.0.1` on the resolved `dashboard.port`.
|
|
271
|
+
*
|
|
272
|
+
* This is the composition root the module comment describes, and the three
|
|
273
|
+
* lines that make it one are the adapter construction, the credential
|
|
274
|
+
* resolution and the `appDir`. **Credentials are core's own provider chain**
|
|
275
|
+
* (`createCredentialProvider`, `packages/core/src/aws/credentials.ts`),
|
|
276
|
+
* resolved here and handed to the adapter, which is the spec's §Analytics
|
|
277
|
+
* dashboard → Credentials: one credential source serves the whole CLI, so a
|
|
278
|
+
* session that works for `deploy` works for the dashboard. It is built with no
|
|
279
|
+
* `override`, matching `transform/entry.ts`: that flag substitutes an
|
|
280
|
+
* emulator's dummy `test`/`test` pair when a real chain fails, and the
|
|
281
|
+
* dashboard has no emulator to talk to - the adapter attaches a real S3 Tables
|
|
282
|
+
* ARN with no endpoint override at all, so dummy credentials would turn "you
|
|
283
|
+
* are not logged in" into an opaque authorisation failure from AWS.
|
|
284
|
+
*
|
|
285
|
+
* Constructing the adapter touches neither the network nor the native library
|
|
286
|
+
* - it opens its connection lazily on the first query - so the listener is
|
|
287
|
+
* bound and the URL is printed before AWS is ever consulted.
|
|
288
|
+
*
|
|
289
|
+
* The `finally` is the shutdown path, and it covers both ways out: the signal
|
|
290
|
+
* {@link untilStopped} waits for, and a failure raised while waiting. Either
|
|
291
|
+
* way `close()` is awaited, so the port is released before this function
|
|
292
|
+
* returns and the next `analytics dashboard` binds it again.
|
|
293
|
+
*/
|
|
294
|
+
export async function dashboard(ctx) {
|
|
295
|
+
const config = resolveAnalyticsConfig(ctx);
|
|
296
|
+
const server = await createDashboardServer({
|
|
297
|
+
query: createDuckDbAnalyticsQuery({ ctx, credentials: createCredentialProvider({}) }),
|
|
298
|
+
config: ctx.pluginConfig,
|
|
299
|
+
port: config.dashboard.port,
|
|
300
|
+
appDir: dashboardAppDir(),
|
|
301
|
+
fs: ctx.ports.fs,
|
|
302
|
+
});
|
|
303
|
+
ctx.logger.info(`analytics dashboard on ${server.url} - press Ctrl+C to stop`);
|
|
304
|
+
try {
|
|
305
|
+
const signal = await untilStopped();
|
|
306
|
+
ctx.logger.info(`${signal} received - stopping the analytics dashboard`);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
await server.close();
|
|
310
|
+
ctx.logger.ok(`analytics dashboard stopped; port ${server.address.port} released`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* `analytics backfill`: the optional, one-shot, idempotent pull of history
|
|
315
|
+
* that predates the Firehose delivery, from the site's CloudWatch log group
|
|
316
|
+
* into the table. Never part of the steady-state pipeline. What it does is
|
|
317
|
+
* `backfill.ts`'s; what this line owns is the two adapters it does it through.
|
|
318
|
+
*
|
|
319
|
+
* `ports` is a defaulted parameter for the reason `status`'s `query` is: every
|
|
320
|
+
* real call site passes nothing and gets the composition root's adapters, and
|
|
321
|
+
* a test hands it a recording pair instead of patching a module. Constructing
|
|
322
|
+
* either adapter touches neither the network nor the native library - both
|
|
323
|
+
* open their connection inside the first statement - so the command's refusal
|
|
324
|
+
* when the plugin's state carries no delivery bound still happens before
|
|
325
|
+
* anything is opened.
|
|
326
|
+
*
|
|
327
|
+
* `args` is accepted and unused: the host calls `run(ctx, args)` for every
|
|
328
|
+
* declared command and this one takes no arguments of its own - the
|
|
329
|
+
* environment is a positional the dispatcher has already consumed.
|
|
330
|
+
*/
|
|
331
|
+
export async function backfill(ctx, _args = [], ports = {
|
|
332
|
+
query: createDuckDbAnalyticsQuery({ ctx, credentials: createCredentialProvider({}) }),
|
|
333
|
+
ingest: createDuckDbAnalyticsIngest({ ctx, credentials: createCredentialProvider({}) }),
|
|
334
|
+
}) {
|
|
335
|
+
await runBackfill(ctx, ports);
|
|
336
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package owns the `analytics` config key end to end: its shape, its
|
|
3
|
+
* defaults, and the validator core's `parseConfig` deliberately does not run
|
|
4
|
+
* (`packages/core/src/config.ts:242` merges an `analytics` block through
|
|
5
|
+
* untouched, so a config carrying one is valid and inert when the plugin is
|
|
6
|
+
* not installed).
|
|
7
|
+
*
|
|
8
|
+
* **Two shapes, and why the seam between them is where it is.**
|
|
9
|
+
* `validateAnalyticsConfig` is the function this package's
|
|
10
|
+
* `Plugin.validateConfig` hands core's raw block to, so whatever it returns is
|
|
11
|
+
* what the host puts on `ctx.pluginConfig` (`packages/core/src/plugin.ts:85-93`).
|
|
12
|
+
* It applies every default it can: `namespace`, `table`, `bots` and
|
|
13
|
+
* `dashboard.port` default to plain literals, so they come back total and no
|
|
14
|
+
* reader downstream re-checks them for `undefined`.
|
|
15
|
+
*
|
|
16
|
+
* `tableBucket` and `saltSecretName` cannot be defaulted there. Both carry the
|
|
17
|
+
* environment, and the SPI hands `validateConfig` the block and nothing else -
|
|
18
|
+
* no `env`, no `siteName`. So an operator's overrides for those two are
|
|
19
|
+
* carried under {@link ENV_DERIVED}, a symbol this module does not export, and
|
|
20
|
+
* {@link resolveAnalyticsConfig} - which takes the plugin context, and
|
|
21
|
+
* therefore always has the environment - is the only way to a bucket name or a
|
|
22
|
+
* salt name.
|
|
23
|
+
*
|
|
24
|
+
* That sealing is load-bearing, not tidiness. Were `tableBucket` merely
|
|
25
|
+
* optional on the validated block, a node downstream could write
|
|
26
|
+
* ``ctx.pluginConfig.tableBucket ?? `${ctx.config.siteName}-analytics` `` and
|
|
27
|
+
* it would compile, typecheck and pass its own tests - and staging would then
|
|
28
|
+
* resolve to production's bucket. With the field absent from the type, that
|
|
29
|
+
* read is `TS2339: Property 'tableBucket' does not exist on type
|
|
30
|
+
* 'AnalyticsConfig'`, which `pnpm typecheck` fails on; `config.test.ts` pins
|
|
31
|
+
* the diagnostic so the seal cannot be opened silently.
|
|
32
|
+
*
|
|
33
|
+
* **Every derived default carries the environment**, matching `deriveNames`'
|
|
34
|
+
* `<env>-<siteName>` prefix (`packages/core/src/config.ts:352`):
|
|
35
|
+
* `tableBucket` is `<env>-<siteName>-analytics` and `saltSecretName` is
|
|
36
|
+
* `<siteName>/<env>/analytics-salt`. Do not "simplify" the environment out of
|
|
37
|
+
* either one. Without it staging and production resolve to the *same* Iceberg
|
|
38
|
+
* table and the same salt, and `blogwright analytics destroy --yes` run in
|
|
39
|
+
* staging issues `DeleteTableBucket` against production's data. Nothing in the
|
|
40
|
+
* system catches that: state is scoped per environment
|
|
41
|
+
* (`state/<env>.analytics.json`), so each environment's state file correctly
|
|
42
|
+
* records the bucket it was told to use and neither can see the collision.
|
|
43
|
+
* AWS gives a second, quieter reason - it "does not recommend using multiple
|
|
44
|
+
* Firehose streams to write data to the same Apache Iceberg table", because
|
|
45
|
+
* Iceberg's optimistic concurrency makes the streams contend.
|
|
46
|
+
*
|
|
47
|
+
* Pure data and pure functions only: no `node:` builtin, no vendor SDK, no
|
|
48
|
+
* `fetch`. See [the change spec's §Configuration → The `analytics`
|
|
49
|
+
* block](../../../.specs/changes/merged/2026-07-26-analytics_plugin.md).
|
|
50
|
+
*/
|
|
51
|
+
import type { PluginContext } from 'blogwright-core';
|
|
52
|
+
/**
|
|
53
|
+
* Whether bot traffic is excluded from dashboard queries. Records are stored
|
|
54
|
+
* either way. Not exported: no consumer needs the bare union today - one can
|
|
55
|
+
* reach the same type as `AnalyticsConfig['bots']`. Export it once a real
|
|
56
|
+
* consumer needs the union by name.
|
|
57
|
+
*/
|
|
58
|
+
type BotHandling = 'flag' | 'filter';
|
|
59
|
+
/**
|
|
60
|
+
* The settings whose defaults are plain literals - the ones needing no
|
|
61
|
+
* environment to resolve, and so total on both the validated block and the
|
|
62
|
+
* resolved config.
|
|
63
|
+
*/
|
|
64
|
+
interface EnvIndependentSettings {
|
|
65
|
+
/** Iceberg namespace holding the table. */
|
|
66
|
+
namespace: string;
|
|
67
|
+
/** Iceberg table name. */
|
|
68
|
+
table: string;
|
|
69
|
+
/** Bot handling for dashboard queries. */
|
|
70
|
+
bots: BotHandling;
|
|
71
|
+
/** Local dashboard settings. */
|
|
72
|
+
dashboard: {
|
|
73
|
+
port: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The two settings whose defaults carry the environment. Never readable off a
|
|
78
|
+
* validated block - see {@link ENV_DERIVED}.
|
|
79
|
+
*/
|
|
80
|
+
interface EnvDerivedOverrides {
|
|
81
|
+
tableBucket?: string | undefined;
|
|
82
|
+
saltSecretName?: string | undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The key an operator's `tableBucket`/`saltSecretName` overrides ride on. A
|
|
86
|
+
* module-private `unique symbol`: TypeScript emits it into `config.d.ts`
|
|
87
|
+
* unexported, so no module outside this one can name it, and neither
|
|
88
|
+
* `ctx.pluginConfig.tableBucket` nor `ctx.pluginConfig[ENV_DERIVED]` compiles
|
|
89
|
+
* anywhere else. That is the point - see this module's doc comment for the
|
|
90
|
+
* `?? <env-less fallback>` line it exists to make unwritable.
|
|
91
|
+
*/
|
|
92
|
+
declare const ENV_DERIVED: unique symbol;
|
|
93
|
+
/**
|
|
94
|
+
* A validated `analytics` block: what {@link validateAnalyticsConfig} returns,
|
|
95
|
+
* and therefore what the host puts on `ctx.pluginConfig` for this plugin. The
|
|
96
|
+
* four settings whose defaults are literals are already applied and total; the
|
|
97
|
+
* two that need the environment are sealed under {@link ENV_DERIVED} and reach
|
|
98
|
+
* a reader only through {@link resolveAnalyticsConfig}.
|
|
99
|
+
*/
|
|
100
|
+
export interface AnalyticsConfig extends EnvIndependentSettings {
|
|
101
|
+
readonly [ENV_DERIVED]: EnvDerivedOverrides;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* An `analytics` block with every default applied, the environment-carrying
|
|
105
|
+
* ones included: the shape every reader downstream keeps a total type on, so
|
|
106
|
+
* no call site re-applies a default or re-checks for `undefined`.
|
|
107
|
+
*/
|
|
108
|
+
export interface ResolvedAnalyticsConfig extends EnvIndependentSettings {
|
|
109
|
+
tableBucket: string;
|
|
110
|
+
saltSecretName: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Default port for the local dashboard server. Exported because the dashboard
|
|
114
|
+
* server binds this exact number: it imports the constant rather than
|
|
115
|
+
* restating it, so the default has one home.
|
|
116
|
+
*/
|
|
117
|
+
export declare const DEFAULT_DASHBOARD_PORT = 4317;
|
|
118
|
+
/**
|
|
119
|
+
* The slice of a plugin context {@link resolveAnalyticsConfig} reads, taken as
|
|
120
|
+
* a `Pick` of core's own `PluginContext` rather than a restatement of it, so
|
|
121
|
+
* the three members cannot drift from the SPI. Any
|
|
122
|
+
* `PluginContext<AnalyticsConfig>` satisfies it, so a node passes `ctx`
|
|
123
|
+
* straight through. Not exported: a caller passes its context and never names
|
|
124
|
+
* this type.
|
|
125
|
+
*/
|
|
126
|
+
type AnalyticsConfigContext = Pick<PluginContext<AnalyticsConfig>, 'env' | 'config' | 'pluginConfig'>;
|
|
127
|
+
/**
|
|
128
|
+
* Validate a raw `analytics` config block, boundary-checked as `unknown`
|
|
129
|
+
* because it comes off `parseConfigDocument`'s `raw` half (a plugin's block
|
|
130
|
+
* has no type until its own package narrows it). An absent block validates as
|
|
131
|
+
* an empty one, so installing the plugin without writing an `analytics` key is
|
|
132
|
+
* valid.
|
|
133
|
+
*
|
|
134
|
+
* Applies the four literal defaults, so `namespace`, `table`, `bots` and
|
|
135
|
+
* `dashboard.port` are total on the returned block. `tableBucket` and
|
|
136
|
+
* `saltSecretName` are validated here too, but sealed under
|
|
137
|
+
* {@link ENV_DERIVED}: their defaults carry the environment, which this
|
|
138
|
+
* signature does not, so {@link resolveAnalyticsConfig} is where they resolve.
|
|
139
|
+
*
|
|
140
|
+
* Raises in the repo's vocabulary (`packages/core/src/config.ts:274-340`),
|
|
141
|
+
* naming the offending key and value.
|
|
142
|
+
*/
|
|
143
|
+
export declare function validateAnalyticsConfig(raw: unknown): AnalyticsConfig;
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the environment-carrying settings a validated block sealed, giving
|
|
146
|
+
* the total config every reader downstream holds. Takes the plugin context
|
|
147
|
+
* rather than a `{ env, siteName }` pair, so the site identity is read out of
|
|
148
|
+
* `ctx` in one place instead of at each call site and no caller can supply an
|
|
149
|
+
* environment other than the one it is running in.
|
|
150
|
+
*
|
|
151
|
+
* Raises when handed a block the validator did not produce - see
|
|
152
|
+
* {@link unsealEnvDerivedOverrides}.
|
|
153
|
+
*
|
|
154
|
+
* The derived bucket name is length-checked here, where it is derived, exactly
|
|
155
|
+
* as `deriveNames` checks the site bucket it derives
|
|
156
|
+
* (`packages/core/src/config.ts:355`). Only the length is checked: `env` and
|
|
157
|
+
* `siteName` are already held to `^[a-z0-9-]+$` by `deriveNames` and
|
|
158
|
+
* `validateConfig`, and a second character check here would be a second home
|
|
159
|
+
* for a rule core already owns.
|
|
160
|
+
*/
|
|
161
|
+
export declare function resolveAnalyticsConfig(ctx: AnalyticsConfigContext): ResolvedAnalyticsConfig;
|
|
162
|
+
export {};
|