@aws-blocks/bb-metrics 0.1.1 → 0.1.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/DESIGN.md ADDED
@@ -0,0 +1,130 @@
1
+ # Metrics — Design
2
+
3
+ Design document for Metrics. For usage, see [README.md](./README.md).
4
+
5
+ **Package:** `@aws-blocks/bb-metrics`
6
+ **Type:** Primitive (no new infrastructure created)
7
+ **AWS Service:** Amazon CloudWatch Metrics (via Embedded Metric Format)
8
+
9
+ ## Key Design Decision: EMF over PutMetricData
10
+
11
+ Metrics are emitted using **CloudWatch Embedded Metric Format (EMF)** rather than calling the `PutMetricData` API directly.
12
+
13
+ ### Why EMF?
14
+
15
+ | Concern | PutMetricData | EMF |
16
+ |---------|---------------|-----|
17
+ | **Latency impact** | HTTP call to CloudWatch (~5-50ms) | Synchronous stdout write (~0ms) |
18
+ | **IAM permissions** | Requires `cloudwatch:PutMetricData` | Uses CloudWatch Logs (Lambda has this by default) |
19
+ | **Batching** | Developer must buffer and flush | CloudWatch extracts metrics from log lines automatically |
20
+ | **Error handling** | Network failures require retries | Stdout never fails (kernel-buffered) |
21
+ | **API surface** | Async (`Promise<void>`) | Sync (`void`) — simpler DX |
22
+ | **Cost** | ~$0.01 per 1000 PutMetricData API calls + metric storage | Log ingestion (already paid) + metric storage only |
23
+ | **Cold start** | AWS SDK init adds ~100ms | Zero additional cold start |
24
+
25
+ ### Trade-offs
26
+
27
+ - **No aggregation control:** EMF extracts metrics at the data point level. If you emit `Latency: 42ms` 1000 times in one invocation, CloudWatch stores 1000 data points (or groups them into a single EMF document with multiple values). With PutMetricData you could pre-aggregate into a StatisticSet.
28
+ - **Log volume:** Each emit writes a JSON line to CloudWatch Logs. High-frequency metrics increase log volume (and log storage cost). Mitigated by CloudWatch Logs Infrequent Access class or retention policies.
29
+ - **Maximum metrics per document:** EMF supports up to 100 metrics per JSON document. The `emitBatch` limit of 100 matches this constraint (vs PutMetricData's 1000 per call).
30
+
31
+ ### Why the trade-offs are acceptable
32
+
33
+ 1. Most BB users emit <100 metrics per invocation — log volume is negligible.
34
+ 2. The simplicity of a synchronous, zero-config API far outweighs the rare need for client-side aggregation.
35
+ 3. Lambda log costs are typically dominated by application logs, not metric EMF lines.
36
+
37
+ ## Architecture
38
+
39
+ ### Runtime Flow (AWS + Local)
40
+
41
+ ```
42
+ emit('Count', 1, { dimensions: { endpoint: '/api' } })
43
+
44
+ ├─ validateMetricName('Count')
45
+ ├─ mergeDimensions(defaults, { endpoint: '/api' })
46
+ ├─ validateDimensions(merged)
47
+
48
+ └─ writeEmf(namespace, [{ name, value, unit, dimensions, timestamp, resolution }])
49
+
50
+ ├─ groupByDimensions(metrics) // EMF requires same dims per entry
51
+
52
+ └─ for each group:
53
+ process.stdout.write(JSON.stringify(emfPayload) + '\n')
54
+ ```
55
+
56
+ ### EMF Document Structure
57
+
58
+ ```json
59
+ {
60
+ "_aws": {
61
+ "Timestamp": 1718450000000,
62
+ "CloudWatchMetrics": [{
63
+ "Namespace": "MyApp/Orders",
64
+ "Dimensions": [["service", "endpoint"]],
65
+ "Metrics": [
66
+ { "Name": "RequestCount", "Unit": "Count", "StorageResolution": 60 },
67
+ { "Name": "Latency", "Unit": "Milliseconds", "StorageResolution": 60 }
68
+ ]
69
+ }]
70
+ },
71
+ "service": "orders",
72
+ "endpoint": "/api",
73
+ "RequestCount": 1,
74
+ "Latency": 42
75
+ }
76
+ ```
77
+
78
+ ### Dimension Grouping
79
+
80
+ EMF requires all metrics in a single `CloudWatchMetrics` entry to share the same dimension keys and values. When `emitBatch` receives metrics with different dimension sets, they are grouped and written as separate JSON lines.
81
+
82
+ ### Child Emitters
83
+
84
+ `child(dimensions)` returns a lightweight `ChildMetrics` object (not a Scope node) that:
85
+ - Inherits the parent's namespace
86
+ - Merges the provided dimensions on top of the parent's `defaultDimensions`
87
+ - Supports further nesting via `child()` on the child itself
88
+
89
+ This enables per-request or per-endpoint metric scoping without creating new Scope nodes.
90
+
91
+ ## Infrastructure (CDK)
92
+
93
+ Unlike most Building Blocks, Metrics does **not** create any AWS resources. CloudWatch namespaces are created implicitly on first metric data point arrival.
94
+
95
+ The CDK construct creates no AWS resources and adds no environment variables or IAM grants. EMF uses CloudWatch Logs (which Lambda already has), so no `cloudwatch:PutMetricData` grant is needed. The construct only:
96
+ 1. **Resolves the namespace:** Computes the resolved `namespace` and exposes it as a readonly property.
97
+ 2. **Exposes `defaultDimensions`:** Exposes `defaultDimensions` as a readonly property so that other CDK-time consumers (like the Dashboard BB) can read them and build matching CloudWatch widget queries.
98
+
99
+ ### Namespace Resolution Order
100
+
101
+ ```
102
+ ExternalMetricsRef.namespace (fromExisting)
103
+ → options.namespace (constructor arg)
104
+ → scope.fullId (default)
105
+ ```
106
+
107
+ ## Mock Implementation
108
+
109
+ There is no separate mock — the AWS runtime (`index.aws.ts`) and the mock (`index.mock.ts`) are identical. Both write EMF JSON to stdout. In local dev, the output is visible in the terminal; in Lambda, it is captured by CloudWatch Logs.
110
+
111
+ This is unique among Building Blocks. Most BBs need a mock because their AWS runtime makes network calls (DynamoDB, S3, SQS, etc.). Metrics via EMF only writes to stdout, which works identically in all environments.
112
+
113
+ ### Mock vs AWS Behavior Differences
114
+
115
+ | Behavior Difference | Impact | Mitigation |
116
+ |------------|--------|------------|
117
+ | No CloudWatch extraction | Metrics are written but not extracted into CloudWatch locally | Expected — local dev is for correctness testing, not dashboards |
118
+ | No alarms | Threshold breaches are not detected locally | Document the gap — alarms require CloudWatch infrastructure |
119
+ | No dashboards | Cannot preview dashboard visualizations locally | Document the gap — dashboards are a CloudWatch console feature |
120
+ | No namespace-scoped IAM | Permission errors only surface in AWS | IAM is handled by CDK grants automatically |
121
+ | No log retention limits | Stdout grows unbounded in local dev | Terminal scrollback is the natural limit |
122
+
123
+ ## Validation
124
+
125
+ All validation matches CloudWatch constraints:
126
+ - **Metric name:** Non-empty, max 1024 characters
127
+ - **Dimensions:** Max 30 key-value pairs, non-empty keys and values, max 1024 chars each
128
+ - **Batch size:** Max 100 metrics per `emitBatch` call (EMF document limit)
129
+
130
+ Validation runs synchronously before the stdout write. Failures throw typed errors (`MetricsErrors.*`) that can be caught with `isBlocksError()`.
package/README.md CHANGED
@@ -6,6 +6,8 @@ Custom application metrics backed by Amazon CloudWatch (via Embedded Metric Form
6
6
 
7
7
  **When NOT to use:** If you need structured log output, use `Logging`. If you need distributed request tracing, use `Tracing`. If you need to store time-series data for querying, use `Database` or `DistributedTable`.
8
8
 
9
+ > Design & mock parity details: [DESIGN.md](./DESIGN.md)
10
+
9
11
  ## API
10
12
 
11
13
  ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"index.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EACX,cAAc,EACd,WAAW,EACX,WAAW,EAGX,kBAAkB,EAClB,cAAc,EACd,MAAM,YAAY,CAAC;AAUpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EACX,cAAc,EACd,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,qBAAa,OAAQ,SAAQ,KAAM,YAAW,cAAc;IAC3D,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE7D,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;IAUpE;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IAe9D;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;IAqBvC;;OAEG;IACH,KAAK,IAAI,IAAI;IAEb;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,cAAc;IAOzD;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB;CAG1D"}
1
+ {"version":3,"file":"index.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EACX,cAAc,EACd,WAAW,EACX,WAAW,EAGX,kBAAkB,EAClB,cAAc,EACd,MAAM,YAAY,CAAC;AAWpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EACX,cAAc,EACd,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,qBAAa,OAAQ,SAAQ,KAAM,YAAW,cAAc;IAC3D,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE7D,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;IAUpE;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IAe9D;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;IAqBvC;;OAEG;IACH,KAAK,IAAI,IAAI;IAEb;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,cAAc;IAOzD;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB;CAG1D"}
package/dist/index.aws.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { Scope } from '@aws-blocks/core';
4
+ import { BB_NAME, BB_VERSION } from './version.js';
4
5
  import { validateMetricName, validateDimensions, validateBatchSize, validateNamespace, mergeDimensions, } from './validation.js';
5
6
  import { Logger } from '@aws-blocks/bb-logger';
6
7
  export { MetricsErrors } from './errors.js';
@@ -61,7 +62,7 @@ export class Metrics extends Scope {
61
62
  /** @internal Logger for internal operations. Defaults to error-level when not provided. */
62
63
  log;
63
64
  constructor(scope, id, options) {
64
- super(id, { parent: scope });
65
+ super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
65
66
  this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
66
67
  this.namespace = options?.metrics?.namespace
67
68
  ?? options?.namespace
@@ -2,7 +2,14 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { test, describe, beforeEach, afterEach } from 'node:test';
4
4
  import assert from 'node:assert';
5
+ import { readFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { Scope } from '@aws-blocks/core';
5
9
  import { Metrics, MetricsErrors } from './index.mock.js';
10
+ import { Metrics as AwsMetrics } from './index.aws.js';
11
+ import { BB_NAME, BB_VERSION } from './version.js';
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
13
  // ── Helpers ─────────────────────────────────────────────────────────────────
7
14
  let stdoutLines = [];
8
15
  let origStdoutWrite;
@@ -493,3 +500,42 @@ describe('scope integration', () => {
493
500
  assert.ok(typeof m.child === 'function');
494
501
  });
495
502
  });
503
+ // ── Telemetry Registration ──────────────────────────────────────────────────
504
+ /**
505
+ * `Scope.getRegisteredBlocks()` only names a block whose `bbName` is in
506
+ * OFFICIAL_BB_NAMES, and that set is generated from the umbrella's
507
+ * `aws-blocks.vendorize` map. These tests pin the three coupled artifacts to
508
+ * each other: the block's generated BB_NAME, its vendorize entry, and the
509
+ * generated name set. A block that omits `bbMeta` still constructs fine and
510
+ * every other test still passes, so that gap is only visible here.
511
+ *
512
+ * Imported through `./index.mock.js`, the package's default entry, which
513
+ * re-exports the AWS runtime class — so both conditions resolve to the class
514
+ * asserted here.
515
+ */
516
+ describe('telemetry registration', () => {
517
+ beforeEach(() => {
518
+ Scope._resetRegistry();
519
+ });
520
+ test('BB_NAME is the name the vendorize map and OFFICIAL_BB_NAMES carry', () => {
521
+ assert.strictEqual(BB_NAME, 'Metrics');
522
+ });
523
+ test('BB_VERSION tracks the package version', () => {
524
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
525
+ assert.strictEqual(BB_VERSION, pkg.version);
526
+ });
527
+ test('the default entry re-exports the AWS runtime class', () => {
528
+ assert.strictEqual(Metrics, AwsMetrics);
529
+ });
530
+ test('an instance carries bbName and bbVersion', () => {
531
+ const m = new Metrics(fakeScope, 'metrics');
532
+ assert.strictEqual(m.bbName, BB_NAME);
533
+ assert.strictEqual(m.bbVersion, BB_VERSION);
534
+ });
535
+ test('registers as an official block, so telemetry is allowed to name it', () => {
536
+ new Metrics(fakeScope, 'metrics');
537
+ const { blocks, customBlocksCount } = Scope.getRegisteredBlocks();
538
+ assert.deepStrictEqual(blocks.filter(b => b.name === BB_NAME), [{ name: BB_NAME, version: BB_VERSION }]);
539
+ assert.strictEqual(customBlocksCount, 0, 'must not be filtered out as an unnamed custom block');
540
+ });
541
+ });
@@ -0,0 +1,3 @@
1
+ export declare const BB_NAME = "Metrics";
2
+ export declare const BB_VERSION = "0.1.3";
3
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,OAAO,YAAY,CAAC;AACjC,eAAO,MAAM,UAAU,UAAU,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
+ export const BB_NAME = 'Metrics';
3
+ export const BB_VERSION = '0.1.3';
package/package.json CHANGED
@@ -1,12 +1,22 @@
1
1
  {
2
2
  "name": "@aws-blocks/bb-metrics",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/aws-devtools-labs/aws-blocks.git",
7
+ "directory": "packages/bb-metrics"
8
+ },
9
+ "homepage": "https://github.com/aws-devtools-labs/aws-blocks/tree/main/packages/bb-metrics#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/aws-devtools-labs/aws-blocks/issues"
12
+ },
4
13
  "author": "Amazon Web Services",
5
14
  "license": "Apache-2.0",
6
15
  "type": "module",
7
16
  "files": [
8
17
  "dist",
9
18
  "README.md",
19
+ "DESIGN.md",
10
20
  "src",
11
21
  "LICENSE"
12
22
  ],
@@ -23,12 +33,13 @@
23
33
  }
24
34
  },
25
35
  "scripts": {
36
+ "prebuild": "node ../../scripts/generate-version.mjs Metrics",
26
37
  "build": "tsc --build",
27
38
  "test": "node --test dist/**/*.test.js"
28
39
  },
29
40
  "dependencies": {
30
- "@aws-blocks/core": "^0.1.1",
31
- "@aws-blocks/bb-logger": "^0.1.1"
41
+ "@aws-blocks/core": "^0.1.17",
42
+ "@aws-blocks/bb-logger": "^0.1.3"
32
43
  },
33
44
  "devDependencies": {
34
45
  "@types/node": "^20.0.0",
package/src/index.aws.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  MetricsEmitter,
14
14
  } from './types.js';
15
15
  import { MetricsErrors } from './errors.js';
16
+ import { BB_NAME, BB_VERSION } from './version.js';
16
17
  import {
17
18
  validateMetricName,
18
19
  validateDimensions,
@@ -94,7 +95,7 @@ export class Metrics extends Scope implements MetricsEmitter {
94
95
  protected log: ChildLogger;
95
96
 
96
97
  constructor(scope: ScopeParent, id: string, options?: MetricsOptions) {
97
- super(id, { parent: scope });
98
+ super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
98
99
  this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
99
100
  this.namespace = options?.metrics?.namespace
100
101
  ?? options?.namespace
package/src/index.test.ts CHANGED
@@ -3,8 +3,16 @@
3
3
 
4
4
  import { test, describe, beforeEach, afterEach } from 'node:test';
5
5
  import assert from 'node:assert';
6
+ import { readFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { Scope } from '@aws-blocks/core';
6
10
  import { Metrics, MetricsErrors } from './index.mock.js';
11
+ import { Metrics as AwsMetrics } from './index.aws.js';
7
12
  import type { MetricsEmitter } from './types.js';
13
+ import { BB_NAME, BB_VERSION } from './version.js';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
16
 
9
17
  // ── Helpers ─────────────────────────────────────────────────────────────────
10
18
 
@@ -630,3 +638,52 @@ describe('scope integration', () => {
630
638
  assert.ok(typeof m.child === 'function');
631
639
  });
632
640
  });
641
+
642
+ // ── Telemetry Registration ──────────────────────────────────────────────────
643
+
644
+ /**
645
+ * `Scope.getRegisteredBlocks()` only names a block whose `bbName` is in
646
+ * OFFICIAL_BB_NAMES, and that set is generated from the umbrella's
647
+ * `aws-blocks.vendorize` map. These tests pin the three coupled artifacts to
648
+ * each other: the block's generated BB_NAME, its vendorize entry, and the
649
+ * generated name set. A block that omits `bbMeta` still constructs fine and
650
+ * every other test still passes, so that gap is only visible here.
651
+ *
652
+ * Imported through `./index.mock.js`, the package's default entry, which
653
+ * re-exports the AWS runtime class — so both conditions resolve to the class
654
+ * asserted here.
655
+ */
656
+ describe('telemetry registration', () => {
657
+ beforeEach(() => {
658
+ Scope._resetRegistry();
659
+ });
660
+
661
+ test('BB_NAME is the name the vendorize map and OFFICIAL_BB_NAMES carry', () => {
662
+ assert.strictEqual(BB_NAME, 'Metrics');
663
+ });
664
+
665
+ test('BB_VERSION tracks the package version', () => {
666
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
667
+ assert.strictEqual(BB_VERSION, pkg.version);
668
+ });
669
+
670
+ test('the default entry re-exports the AWS runtime class', () => {
671
+ assert.strictEqual(Metrics, AwsMetrics);
672
+ });
673
+
674
+ test('an instance carries bbName and bbVersion', () => {
675
+ const m = new Metrics(fakeScope, 'metrics');
676
+ assert.strictEqual(m.bbName, BB_NAME);
677
+ assert.strictEqual(m.bbVersion, BB_VERSION);
678
+ });
679
+
680
+ test('registers as an official block, so telemetry is allowed to name it', () => {
681
+ new Metrics(fakeScope, 'metrics');
682
+ const { blocks, customBlocksCount } = Scope.getRegisteredBlocks();
683
+ assert.deepStrictEqual(
684
+ blocks.filter(b => b.name === BB_NAME),
685
+ [{ name: BB_NAME, version: BB_VERSION }],
686
+ );
687
+ assert.strictEqual(customBlocksCount, 0, 'must not be filtered out as an unnamed custom block');
688
+ });
689
+ });
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
+ export const BB_NAME = 'Metrics';
3
+ export const BB_VERSION = '0.1.3';