@decocms/blocks 7.0.0

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.
Files changed (111) hide show
  1. package/package.json +97 -0
  2. package/src/cms/applySectionConventions.ts +128 -0
  3. package/src/cms/blockSource.test.ts +67 -0
  4. package/src/cms/blockSource.ts +124 -0
  5. package/src/cms/index.ts +104 -0
  6. package/src/cms/layoutCacheRace.test.ts +146 -0
  7. package/src/cms/loadDecofileDirectory.test.ts +52 -0
  8. package/src/cms/loadDecofileDirectory.ts +48 -0
  9. package/src/cms/loader.test.ts +184 -0
  10. package/src/cms/loader.ts +284 -0
  11. package/src/cms/registry.test.ts +118 -0
  12. package/src/cms/registry.ts +252 -0
  13. package/src/cms/resolve.test.ts +547 -0
  14. package/src/cms/resolve.ts +2003 -0
  15. package/src/cms/schema.ts +993 -0
  16. package/src/cms/sectionLoaders.test.ts +409 -0
  17. package/src/cms/sectionLoaders.ts +562 -0
  18. package/src/cms/sectionMixins.test.ts +163 -0
  19. package/src/cms/sectionMixins.ts +153 -0
  20. package/src/hooks/LazySection.tsx +121 -0
  21. package/src/hooks/LiveControls.tsx +122 -0
  22. package/src/hooks/RenderSection.test.tsx +81 -0
  23. package/src/hooks/RenderSection.tsx +91 -0
  24. package/src/hooks/SectionErrorFallback.tsx +97 -0
  25. package/src/hooks/index.ts +4 -0
  26. package/src/index.ts +8 -0
  27. package/src/matchers/builtins.test.ts +251 -0
  28. package/src/matchers/builtins.ts +437 -0
  29. package/src/matchers/countryNames.ts +104 -0
  30. package/src/matchers/override.test.ts +205 -0
  31. package/src/matchers/override.ts +136 -0
  32. package/src/matchers/posthog.ts +154 -0
  33. package/src/middleware/decoState.ts +55 -0
  34. package/src/middleware/healthMetrics.ts +133 -0
  35. package/src/middleware/hydrationContext.test.ts +61 -0
  36. package/src/middleware/hydrationContext.ts +79 -0
  37. package/src/middleware/index.ts +88 -0
  38. package/src/middleware/liveness.ts +21 -0
  39. package/src/middleware/observability.test.ts +238 -0
  40. package/src/middleware/observability.ts +620 -0
  41. package/src/middleware/validateSection.test.ts +147 -0
  42. package/src/middleware/validateSection.ts +100 -0
  43. package/src/sdk/abTesting.test.ts +326 -0
  44. package/src/sdk/abTesting.ts +499 -0
  45. package/src/sdk/analytics.ts +77 -0
  46. package/src/sdk/cacheHeaders.test.ts +115 -0
  47. package/src/sdk/cacheHeaders.ts +424 -0
  48. package/src/sdk/cachedLoader.ts +364 -0
  49. package/src/sdk/clx.ts +5 -0
  50. package/src/sdk/cn.test.ts +34 -0
  51. package/src/sdk/cn.ts +28 -0
  52. package/src/sdk/composite.test.ts +121 -0
  53. package/src/sdk/composite.ts +114 -0
  54. package/src/sdk/cookie.test.ts +108 -0
  55. package/src/sdk/cookie.ts +129 -0
  56. package/src/sdk/crypto.ts +185 -0
  57. package/src/sdk/csp.ts +59 -0
  58. package/src/sdk/djb2.ts +20 -0
  59. package/src/sdk/encoding.test.ts +71 -0
  60. package/src/sdk/encoding.ts +47 -0
  61. package/src/sdk/env.ts +31 -0
  62. package/src/sdk/http.test.ts +71 -0
  63. package/src/sdk/http.ts +124 -0
  64. package/src/sdk/index.ts +90 -0
  65. package/src/sdk/inflightTimeout.test.ts +35 -0
  66. package/src/sdk/inflightTimeout.ts +53 -0
  67. package/src/sdk/instrumentedFetch.test.ts +559 -0
  68. package/src/sdk/instrumentedFetch.ts +339 -0
  69. package/src/sdk/invoke.test.ts +115 -0
  70. package/src/sdk/invoke.ts +260 -0
  71. package/src/sdk/logger.test.ts +432 -0
  72. package/src/sdk/logger.ts +304 -0
  73. package/src/sdk/mergeCacheControl.ts +150 -0
  74. package/src/sdk/normalizeUrls.ts +91 -0
  75. package/src/sdk/observability.ts +109 -0
  76. package/src/sdk/otel.test.ts +526 -0
  77. package/src/sdk/otel.ts +981 -0
  78. package/src/sdk/otelAdapters/clickhouseCollector.ts +65 -0
  79. package/src/sdk/otelAdapters.test.ts +89 -0
  80. package/src/sdk/otelAdapters.ts +144 -0
  81. package/src/sdk/otelHttpLog.test.ts +457 -0
  82. package/src/sdk/otelHttpLog.ts +419 -0
  83. package/src/sdk/otelHttpMeter.test.ts +292 -0
  84. package/src/sdk/otelHttpMeter.ts +506 -0
  85. package/src/sdk/otelHttpTracer.test.ts +474 -0
  86. package/src/sdk/otelHttpTracer.ts +543 -0
  87. package/src/sdk/redirects.ts +225 -0
  88. package/src/sdk/requestContext.ts +281 -0
  89. package/src/sdk/requestContextStorage.browser.test.ts +29 -0
  90. package/src/sdk/requestContextStorage.browser.ts +43 -0
  91. package/src/sdk/requestContextStorage.ts +35 -0
  92. package/src/sdk/retry.ts +45 -0
  93. package/src/sdk/serverTimings.ts +68 -0
  94. package/src/sdk/signal.ts +42 -0
  95. package/src/sdk/sitemap.ts +160 -0
  96. package/src/sdk/urlRedaction.test.ts +73 -0
  97. package/src/sdk/urlRedaction.ts +82 -0
  98. package/src/sdk/urlUtils.ts +134 -0
  99. package/src/sdk/useDevice.test.ts +130 -0
  100. package/src/sdk/useDevice.ts +109 -0
  101. package/src/sdk/useDeviceContext.tsx +108 -0
  102. package/src/sdk/useId.ts +7 -0
  103. package/src/sdk/useScript.test.ts +128 -0
  104. package/src/sdk/useScript.ts +210 -0
  105. package/src/sdk/useSuggestions.test.ts +230 -0
  106. package/src/sdk/useSuggestions.ts +188 -0
  107. package/src/sdk/wrapCaughtErrors.ts +107 -0
  108. package/src/setup.ts +119 -0
  109. package/src/types/index.ts +39 -0
  110. package/src/types/widgets.ts +14 -0
  111. package/tsconfig.json +7 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Placeholder for a future OTel-collector based exporter that ships
3
+ * logs / metrics / traces from this Worker to a co-deployed OTel Collector
4
+ * gateway, which in turn batches into ClickHouse.
5
+ *
6
+ * **NOT IMPLEMENTED in 5.0.0.** The framework today defaults to CF-native
7
+ * observability: logs and traces appear in the Cloudflare dashboard
8
+ * (`observability.{logs,traces}` in `wrangler.jsonc`), and metrics go to
9
+ * Workers Analytics Engine. When the platform-side ClickHouse + collector
10
+ * deployment lands, this file gets the real OTLP/HTTP exporter wiring back
11
+ * — mirror the implementation that lived in `src/sdk/otelAdapters.ts` and
12
+ * `src/sdk/sampler.ts` at the pre-5.0.0 git tag.
13
+ *
14
+ * The expected shape (`endpoint`, optional `headers`, optional `resource`,
15
+ * `exportIntervalMillis`) is recorded here so site-side bootstrapping code
16
+ * can compile against the future surface today and we don't churn the
17
+ * `OtelOptions` type when the implementation lands.
18
+ *
19
+ * Reference: https://clickhouse.com/docs/observability/integrating-opentelemetry
20
+ *
21
+ * Expected wiring once implemented:
22
+ * - Worker → OTLP/HTTP → OTel Collector (gateway, per region)
23
+ * - Collector pipelines:
24
+ * receivers[otlp] → processors[batch, memory_limiter] → exporters[clickhouse]
25
+ * - The Worker uses the collector URL as `endpoint`, NOT a direct
26
+ * ClickHouse URL. The collector owns the credentials, retry policy,
27
+ * and schema mapping.
28
+ */
29
+
30
+ export interface ClickhouseCollectorOptions {
31
+ /**
32
+ * OTel Collector OTLP/HTTP endpoint, e.g. `https://otel-collector.internal`.
33
+ * The Worker never talks to ClickHouse directly — it talks to the collector.
34
+ */
35
+ endpoint: string;
36
+ /** Optional bearer token / mTLS identity headers — collector-defined. */
37
+ headers?: Record<string, string>;
38
+ /**
39
+ * Resource attributes to stamp on every emitted record. Keep narrow
40
+ * (`service.name`, `deco.runtime.version`, `deployment.environment`) —
41
+ * higher-cardinality attributes belong on individual spans / log records.
42
+ */
43
+ resource?: Record<string, string>;
44
+ /** Periodic metric reader push interval in ms. Defaults to 60_000 once wired. */
45
+ exportIntervalMillis?: number;
46
+ }
47
+
48
+ /**
49
+ * Returns nothing today — throws to make accidental usage loud and obvious.
50
+ *
51
+ * The intended return shape is `{ logger: LoggerAdapter, meter: MeterAdapter,
52
+ * tracer: TracerAdapter, flush: () => Promise<void> }` so the wiring inside
53
+ * `instrumentWorker` can compose this with the AE meter (dual-emit) and the
54
+ * default console logger. None of that ships in 5.0.0.
55
+ */
56
+ export function createClickhouseCollectorAdapter(_options: ClickhouseCollectorOptions): never {
57
+ throw new Error(
58
+ "createClickhouseCollectorAdapter is not implemented in @decocms/start@5.x. " +
59
+ "ClickHouse + OTel-collector integration is on the roadmap. " +
60
+ "Until then, use Cloudflare-native observability " +
61
+ "(observability.{logs,traces} in wrangler.jsonc) plus the Workers " +
62
+ "Analytics Engine meter wired by instrumentWorker(). Track progress " +
63
+ "at https://github.com/decocms/deco-start/issues.",
64
+ );
65
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Coverage for `createAnalyticsEngineMeterAdapter` and the
3
+ * `setRuntimeEnv` / `getRuntimeEnv` helpers.
4
+ *
5
+ * As of 5.0.0 this file no longer covers `createOtelLoggerAdapter` /
6
+ * `createOtelMeterAdapter` / `flushOtelProviders` — those exporters were
7
+ * removed when the framework converged on Cloudflare-native log + trace
8
+ * capture. AE remains the in-Worker metrics path.
9
+ */
10
+ import { afterEach, describe, expect, it, vi } from "vitest";
11
+ import { createAnalyticsEngineMeterAdapter, setRuntimeEnv } from "./otelAdapters";
12
+ import { RequestContext } from "./requestContext";
13
+
14
+ describe("createAnalyticsEngineMeterAdapter", () => {
15
+ afterEach(() => vi.restoreAllMocks());
16
+
17
+ it("no-ops when binding is missing (does not throw)", () => {
18
+ const meter = createAnalyticsEngineMeterAdapter({ bindingName: "MISSING" });
19
+ expect(() => meter.counterInc("foo", 1)).not.toThrow();
20
+ expect(() => meter.gaugeSet?.("bar", 5)).not.toThrow();
21
+ expect(() => meter.histogramRecord?.("baz", 100)).not.toThrow();
22
+ });
23
+
24
+ it("writes one data point per call to the resolved binding", () => {
25
+ const writeDataPoint = vi.fn();
26
+ const binding = { writeDataPoint };
27
+
28
+ const meter = createAnalyticsEngineMeterAdapter({ binding });
29
+
30
+ meter.counterInc("deco.cache.hits", 1, { profile: "product" });
31
+ meter.histogramRecord?.("http.server.request.duration", 150, {
32
+ method: "GET",
33
+ path: "/",
34
+ status: 200,
35
+ });
36
+ meter.gaugeSet?.("custom_gauge", 7, { region: "gru" });
37
+
38
+ expect(writeDataPoint).toHaveBeenCalledTimes(3);
39
+
40
+ // Non-HTTP counter falls back to the metric name as the index.
41
+ expect(writeDataPoint.mock.calls[0]?.[0].indexes).toEqual(["deco.cache.hits"]);
42
+ expect(writeDataPoint.mock.calls[0]?.[0].doubles).toEqual([1]);
43
+ expect(writeDataPoint.mock.calls[0]?.[0].blobs?.[0]).toBe("deco.cache.hits");
44
+
45
+ // For canonical HTTP server histogram, indexes[0] must be the path.
46
+ expect(writeDataPoint.mock.calls[1]?.[0].indexes).toEqual(["/"]);
47
+ expect(writeDataPoint.mock.calls[1]?.[0].doubles).toEqual([150]);
48
+
49
+ // Non-HTTP metrics fall back to the metric name as the index.
50
+ expect(writeDataPoint.mock.calls[2]?.[0].indexes).toEqual(["custom_gauge"]);
51
+ expect(writeDataPoint.mock.calls[2]?.[0].doubles).toEqual([7]);
52
+ });
53
+
54
+ it("resolves binding via RequestContext when not provided directly", () => {
55
+ const writeDataPoint = vi.fn();
56
+ const env = { DECO_METRICS: { writeDataPoint } };
57
+
58
+ RequestContext.run(new Request("https://x.example/"), () => {
59
+ setRuntimeEnv(env);
60
+ const meter = createAnalyticsEngineMeterAdapter();
61
+ // Canonical HTTP server histogram → path is promoted to index[0].
62
+ meter.histogramRecord?.("http.server.request.duration", 1, { path: "/x" });
63
+ expect(writeDataPoint).toHaveBeenCalledOnce();
64
+ expect(writeDataPoint.mock.calls[0]?.[0].indexes).toEqual(["/x"]);
65
+ });
66
+ });
67
+
68
+ it("never throws when binding throws", () => {
69
+ const binding = {
70
+ writeDataPoint: () => {
71
+ throw new Error("AE down");
72
+ },
73
+ };
74
+ const meter = createAnalyticsEngineMeterAdapter({ binding });
75
+ expect(() => meter.counterInc("x", 1)).not.toThrow();
76
+ });
77
+
78
+ it("orders blob columns deterministically by sorted label keys", () => {
79
+ const writeDataPoint = vi.fn();
80
+ const binding = { writeDataPoint };
81
+ const meter = createAnalyticsEngineMeterAdapter({ binding });
82
+
83
+ meter.counterInc("metric", 1, { z: "last", a: "first", m: "mid" });
84
+
85
+ const blobs = writeDataPoint.mock.calls[0]?.[0].blobs as string[];
86
+ // blobs[0] is metric name; remaining must follow sorted label-key order: a, m, z
87
+ expect(blobs).toEqual(["metric", "first", "mid", "last"]);
88
+ });
89
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Metric adapters for `@decocms/start`.
3
+ *
4
+ * The OTLP logger and OTLP meter adapters that lived here through 4.x were
5
+ * removed in 5.0.0. The framework now relies on:
6
+ * - Cloudflare's platform-managed log capture (`console.*` → CF dashboard
7
+ * via `observability.logs`) for log shipping
8
+ * - Cloudflare's auto-instrumented + global-tracer-bridged trace export
9
+ * (`observability.traces`) for trace shipping
10
+ * - Workers Analytics Engine — implemented in this file — for metrics
11
+ *
12
+ * Forwarding to a co-deployed OTel collector (the path to ClickHouse)
13
+ * lives in `./otelAdapters/clickhouseCollector.ts`. That file is a
14
+ * documented stub today; the implementation will reintroduce a thin
15
+ * OTLP/HTTP exporter aimed at the collector — never directly at a
16
+ * destination — when the collector is deployed.
17
+ *
18
+ * The `setRuntimeEnv` / `getRuntimeEnv` helpers stay here because
19
+ * `workerEntry.ts` and the AE adapter both depend on them.
20
+ */
21
+
22
+ import type { MeterAdapter } from "../middleware/observability";
23
+ import { MetricNames } from "../middleware/observability";
24
+ import { RequestContext } from "./requestContext";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Env / binding access
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Per-request access to the Cloudflare Worker `env` bag. Stashed by
32
+ * `workerEntry.ts` at the top of every request via
33
+ * `RequestContext.setBag("__deco_env", env)`.
34
+ *
35
+ * Adapters use this to look up the AE binding (or any future binding-driven
36
+ * destination) without forcing site code to thread `env` through every
37
+ * call site.
38
+ */
39
+ const ENV_BAG_KEY = "__deco_env";
40
+
41
+ export function setRuntimeEnv(env: Record<string, unknown>): void {
42
+ RequestContext.setBag(ENV_BAG_KEY, env);
43
+ }
44
+
45
+ export function getRuntimeEnv(): Record<string, unknown> | undefined {
46
+ return RequestContext.getBag<Record<string, unknown>>(ENV_BAG_KEY);
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // AnalyticsEngineMeterAdapter
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Workers Analytics Engine binding shape. Defined here so we don't need
55
+ * `@cloudflare/workers-types` as a dep.
56
+ */
57
+ export interface AnalyticsEngineDataset {
58
+ writeDataPoint(point: { indexes?: string[]; blobs?: string[]; doubles?: number[] }): void;
59
+ }
60
+
61
+ export interface AnalyticsEngineMeterAdapterOptions {
62
+ /** Env var name holding the AE binding. Defaults to "DECO_METRICS". */
63
+ bindingName?: string;
64
+ /** Pre-resolved binding (mainly for tests). Bypasses RequestContext lookup. */
65
+ binding?: AnalyticsEngineDataset;
66
+ }
67
+
68
+ /**
69
+ * Writes one Analytics Engine data point per metric call.
70
+ *
71
+ * Schema (must stay stable — dashboards and SQL queries depend on it):
72
+ * - `indexes[0]`: a low-cardinality dimension. For request metrics that's
73
+ * the normalized URL path; for cache/resolve metrics that's the metric
74
+ * name itself.
75
+ * - `blobs[0]`: metric name (so a single dataset can hold many metrics).
76
+ * - `blobs[1..]`: stringified label values, in stable label-name order.
77
+ * - `doubles[0]`: the metric value (count, ms, gauge value).
78
+ *
79
+ * AE truncates / charges per data point, so this adapter is intentionally
80
+ * coarse: one point per `counterInc` / `histogramRecord` / `gaugeSet`.
81
+ */
82
+ export function createAnalyticsEngineMeterAdapter(
83
+ options: AnalyticsEngineMeterAdapterOptions = {},
84
+ ): MeterAdapter {
85
+ const bindingName = options.bindingName ?? "DECO_METRICS";
86
+
87
+ function resolveBinding(): AnalyticsEngineDataset | null {
88
+ if (options.binding) return options.binding;
89
+ const env = getRuntimeEnv();
90
+ const b = env?.[bindingName] as AnalyticsEngineDataset | undefined;
91
+ return b ?? null;
92
+ }
93
+
94
+ function write(name: string, value: number, labels?: Record<string, unknown>): void {
95
+ const binding = resolveBinding();
96
+ if (!binding) return; // No-op when binding is missing — never throw.
97
+
98
+ const indexValue = pickIndex(name, labels);
99
+ const blobs: string[] = [name];
100
+ if (labels) {
101
+ // Stable order — sort keys so the same blob index always means the
102
+ // same label across requests. AE has no schema; we enforce one here.
103
+ const keys = Object.keys(labels).sort();
104
+ for (const k of keys) {
105
+ const v = labels[k];
106
+ if (v !== undefined && v !== null) blobs.push(String(v));
107
+ }
108
+ }
109
+
110
+ try {
111
+ binding.writeDataPoint({
112
+ indexes: indexValue ? [indexValue] : undefined,
113
+ blobs,
114
+ doubles: [value],
115
+ });
116
+ } catch {
117
+ /* AE write failed — never fail the request */
118
+ }
119
+ }
120
+
121
+ return {
122
+ counterInc(name, value, labels) {
123
+ write(name, value ?? 1, labels);
124
+ },
125
+ histogramRecord(name, value, labels) {
126
+ write(name, value, labels);
127
+ },
128
+ gaugeSet(name, value, labels) {
129
+ write(name, value, labels);
130
+ },
131
+ };
132
+ }
133
+
134
+ function pickIndex(metricName: string, labels?: Record<string, unknown>): string | undefined {
135
+ if (!labels) return metricName;
136
+ // For HTTP request metrics the natural index is the path (matches the
137
+ // schema: `indexes[0]=path`). For other metrics, fall back to the metric
138
+ // name so the dataset is always queryable by index.
139
+ if (metricName === MetricNames.HTTP_SERVER_REQUEST_DURATION) {
140
+ const p = labels.path;
141
+ if (typeof p === "string" && p.length > 0) return p;
142
+ }
143
+ return metricName;
144
+ }