@prism-d1/cli 1.0.79 → 1.0.80

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 (38) hide show
  1. package/dist/assets/github-workflows/prism-ai-metrics.yml +83 -122
  2. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +66 -0
  3. package/dist/assets/github-workflows/prism-eval-gate.yml +1 -1
  4. package/dist/assets/gitlab-workflows/.gitlab-ci.yml +0 -3
  5. package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +53 -56
  6. package/dist/assets/infra/bin/app.ts +1 -0
  7. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +4 -0
  8. package/dist/assets/infra/lib/dashboard-stack.ts +378 -1235
  9. package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
  10. package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
  11. package/dist/assets/infra/lib/lambda/otel-receiver.ts +249 -35
  12. package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
  13. package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
  14. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +110 -119
  15. package/dist/assets/infra/package.json +2 -1
  16. package/dist/assets/infra/tsconfig.json +9 -2
  17. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
  18. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  19. package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
  20. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  21. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
  22. package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
  23. package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
  24. package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
  25. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
  26. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
  27. package/dist/src/utils/region.d.ts +50 -0
  28. package/dist/src/utils/region.d.ts.map +1 -0
  29. package/dist/src/utils/region.js +62 -0
  30. package/dist/src/utils/region.js.map +1 -0
  31. package/package.json +2 -2
  32. package/dist/assets/eval-harness/README.md +0 -144
  33. package/dist/assets/github-workflows/README.md +0 -110
  34. package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
  35. package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
  36. package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
  37. package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
  38. package/dist/assets/metric-hooks/README.md +0 -107
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Canonical PRISM event schema — the contract between emitters and the pipeline.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * The event payload is a wide union of optional sections that producers fill
7
+ * selectively. Nothing validated it, and every silent failure in this pipeline
8
+ * traced back to that gap:
9
+ *
10
+ * - prism-dora-weekly.yml emitted `dora.lead_time_hours` while the processor
11
+ * read `lead_time_seconds`. The `!= null` guard skipped it, so the
12
+ * LeadTimeForChanges metric was never published and nothing errored.
13
+ * - It emitted `dora.mttr_hours` against `mttr_seconds`, so the MTTR metric
14
+ * was never published at all.
15
+ * - `ai_dora.ai_adoption_rate` matched no key the processor consumed.
16
+ * - The `pr` section was emitted by producers for months but missing from the
17
+ * processor's persisted-section list, so every field was dropped at write.
18
+ * - GitHub emitted `pr` while GitLab emitted `mr` for the same concept.
19
+ *
20
+ * None of those threw. They produced plausible-looking dashboards with missing
21
+ * data. Declaring the shape here and validating against it converts that class
22
+ * of failure from silent to loud.
23
+ */
24
+
25
+ /** Sections persisted to the events table. Anything not listed is DROPPED. */
26
+ export const PERSISTED_SECTIONS = [
27
+ 'metric',
28
+ 'ai_context',
29
+ 'dora',
30
+ 'ai_dora',
31
+ 'eval',
32
+ 'guardrail',
33
+ 'mcp_tool_call',
34
+ 'agent',
35
+ 'security',
36
+ 'security_agent_finding',
37
+ 'security_remediation',
38
+ 'pr',
39
+ ] as const;
40
+
41
+ export type PersistedSection = (typeof PERSISTED_SECTIONS)[number];
42
+
43
+ /**
44
+ * Known field names per section. Used by validateEventShape to flag fields a
45
+ * producer emits that no consumer reads — the `lead_time_hours` class of bug.
46
+ *
47
+ * This is intentionally a denylist-by-omission: unknown fields are WARNED about,
48
+ * not rejected, so adding a field is never a hard outage. The CI guard
49
+ * (scripts/check-metric-coverage.ts) is what fails the build.
50
+ */
51
+ export const SECTION_FIELDS: Record<PersistedSection, readonly string[]> = {
52
+ metric: ['name', 'value', 'unit'],
53
+ ai_context: ['tool', 'model', 'origin', 'origin_source'],
54
+ dora: [
55
+ 'deployment_frequency',
56
+ 'lead_time_seconds',
57
+ 'change_failure_rate',
58
+ 'mttr_seconds',
59
+ 'is_failure_fix',
60
+ ],
61
+ ai_dora: ['eval_gate_pass_rate'],
62
+ eval: ['eval_id', 'rubric', 'result', 'score', 'findings', 'high_findings', 'pr_number', 'criterion_scores'],
63
+ guardrail: ['guardrail_id', 'trigger_category', 'trigger_type', 'action_taken', 'agent_name'],
64
+ mcp_tool_call: ['session_id', 'client_id', 'tool_name', 'scopes', 'authorized', 'risk_level', 'result_status', 'duration_ms'],
65
+ agent: ['agent_name', 'status', 'duration_ms', 'steps_taken', 'tools_invoked', 'tokens_used', 'guardrails_triggered'],
66
+ security: ['alert_type', 'table_name', 'principal_arn', 'read_count'],
67
+ security_agent_finding: [
68
+ 'finding_id', 'phase', 'severity', 'cvss_score', 'title', 'description',
69
+ 'category', 'cwe_id', 'exploit_validated', 'remediation_guidance',
70
+ 'compliance_mappings', 'ai_origin', 'commit_shas', 'pr_number',
71
+ 'commit_sha', 'spec_ref', 'environment', 'found_at', 'remediated_at',
72
+ ],
73
+ security_remediation: [
74
+ 'finding_id', 'severity', 'remediation_time_hours', 'remediated_by_origin',
75
+ 'remediated_by_origin_source', 'fix_commit_shas', 'fix_pr_number', 'finding_phase',
76
+ ],
77
+ pr: [
78
+ 'number', 'author', 'reviews_approved', 'reviews_changes_requested',
79
+ 'total_commits', 'commit_shas',
80
+ ],
81
+ };
82
+
83
+ /**
84
+ * Units matter: these fields have caused real bugs by carrying the wrong unit
85
+ * under a plausible name. A producer emitting `lead_time_hours` is emitting a
86
+ * field this pipeline does not read — the suffix is load-bearing.
87
+ */
88
+ export const UNIT_SUFFIXED_FIELDS = [
89
+ 'lead_time_seconds',
90
+ 'mttr_seconds',
91
+ 'remediation_time_hours',
92
+ 'duration_ms',
93
+ ] as const;
94
+
95
+ export interface ShapeWarning {
96
+ section: string;
97
+ field?: string;
98
+ message: string;
99
+ }
100
+
101
+ /**
102
+ * Non-throwing shape validation. Returns warnings for the processor to log so
103
+ * field drift shows up in CloudWatch Logs Insights instead of vanishing.
104
+ *
105
+ * Deliberately does NOT reject: dropping a real event because a producer added
106
+ * a field would be a worse failure than logging it.
107
+ */
108
+ export function validateEventShape(detail: Record<string, unknown>): ShapeWarning[] {
109
+ const warnings: ShapeWarning[] = [];
110
+ const known = new Set<string>(PERSISTED_SECTIONS);
111
+
112
+ for (const [key, value] of Object.entries(detail)) {
113
+ // Envelope fields live alongside sections and are handled separately.
114
+ if (['team_id', 'repo', 'timestamp', 'prism_level'].includes(key)) continue;
115
+
116
+ if (!known.has(key)) {
117
+ warnings.push({
118
+ section: key,
119
+ message: `section '${key}' is not in PERSISTED_SECTIONS and will be DROPPED at write time`,
120
+ });
121
+ continue;
122
+ }
123
+
124
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
125
+ const allowed = SECTION_FIELDS[key as PersistedSection];
126
+ for (const field of Object.keys(value as Record<string, unknown>)) {
127
+ if (!allowed.includes(field)) {
128
+ // Near-miss on a unit suffix is the highest-signal case.
129
+ const nearMiss = UNIT_SUFFIXED_FIELDS.find(
130
+ f => f.split('_').slice(0, -1).join('_') === field.split('_').slice(0, -1).join('_'),
131
+ );
132
+ warnings.push({
133
+ section: key,
134
+ field,
135
+ message: nearMiss
136
+ ? `'${key}.${field}' is not consumed — did you mean '${nearMiss}'? Unit suffixes are load-bearing.`
137
+ : `'${key}.${field}' is not consumed by any reader`,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return warnings;
144
+ }
@@ -1,3 +1,4 @@
1
+ import { PERSISTED_SECTIONS, validateEventShape } from './event-schema';
1
2
  import {
2
3
  DynamoDBClient,
3
4
  PutItemCommand,
@@ -22,6 +23,28 @@ interface DoraMetrics {
22
23
  lead_time_seconds: number | null;
23
24
  change_failure_rate: number | null;
24
25
  mttr_seconds: number | null;
26
+ /**
27
+ * Per-PR failure-fix label (title matched revert|hotfix|rollback). This is the
28
+ * change-failure-rate NUMERATOR as a fact; the rate and MTTR are computed at
29
+ * query time by the dashboard widgets over the selected window. Emitting a
30
+ * pre-computed rate per PR was meaningless (0% or 100%) and a pre-computed
31
+ * weekly rate hardcoded a 7-day window the dashboard could not override.
32
+ */
33
+ is_failure_fix?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Per-PR facts from prism-ai-metrics.yml. Persisted so the dashboard can
38
+ * aggregate at query time and resolve AI origin by joining commit_shas against
39
+ * the attribution store.
40
+ */
41
+ interface PrDetail {
42
+ number: number;
43
+ author: string;
44
+ reviews_approved: number;
45
+ reviews_changes_requested: number;
46
+ total_commits?: number;
47
+ commit_shas?: string[];
25
48
  }
26
49
 
27
50
  interface AiDoraMetrics {
@@ -95,7 +118,19 @@ interface SecurityAgentFinding {
95
118
  cwe_id: string | null;
96
119
  exploit_validated: boolean;
97
120
  compliance_mappings: string[];
98
- ai_origin: string;
121
+ /**
122
+ * Trailer-derived origin. Legacy — emitters now send `commit_shas` instead
123
+ * and dashboards resolve origin at render time against the attribution
124
+ * store. Retained so events emitted before the cutover still parse.
125
+ */
126
+ ai_origin?: string;
127
+ /**
128
+ * Commit SHAs the finding's PR introduced. Immutable facts — the deferred
129
+ * join reads these at query time, which is correct even when the PR merged
130
+ * before `codeburn sync --attribution` had run for those commits.
131
+ */
132
+ commit_shas?: string[];
133
+ pr_number?: number;
99
134
  spec_ref: string | null;
100
135
  found_at: string;
101
136
  remediated_at: string | null;
@@ -105,7 +140,17 @@ interface SecurityRemediationDetail {
105
140
  finding_id: string;
106
141
  severity: string;
107
142
  remediation_time_hours: number;
143
+ /**
144
+ * Trailer-derived verdict for who fixed the finding. Legacy — reads 'human'
145
+ * for everything once git hooks are removed. Dashboards prefer
146
+ * `fix_commit_shas` and only fall back to this. See the deferred-join note
147
+ * on SecurityAgentFinding.commit_shas.
148
+ */
108
149
  remediated_by_origin: string;
150
+ remediated_by_origin_source?: string;
151
+ /** Fix PR's commit SHAs — joined against the attribution store at render time. */
152
+ fix_commit_shas?: string[];
153
+ fix_pr_number?: number | null;
109
154
  finding_phase: string;
110
155
  }
111
156
 
@@ -134,6 +179,7 @@ interface MetricDetail {
134
179
  security?: SecurityDetail;
135
180
  security_agent_finding?: SecurityAgentFinding;
136
181
  security_remediation?: SecurityRemediationDetail;
182
+ pr?: PrDetail;
137
183
  }
138
184
 
139
185
  interface EventBridgeEvent {
@@ -155,13 +201,10 @@ const METRIC_NAMESPACE = process.env.METRIC_NAMESPACE ?? 'PRISM/D1/Velocity';
155
201
  // ---- Handler ----
156
202
 
157
203
  export async function handler(event: EventBridgeEvent): Promise<void> {
158
- console.log('[metrics-processor] Received event:', JSON.stringify(event, null, 2));
159
-
160
204
  const detailType = event['detail-type'];
161
205
  const detail = event.detail;
162
206
 
163
207
  console.log(`[metrics-processor] detail-type=${detailType} team_id=${detail?.team_id} repo=${detail?.repo} timestamp=${detail?.timestamp}`);
164
- console.log(`[metrics-processor] dora=${JSON.stringify(detail?.dora)} ai_dora=${JSON.stringify(detail?.ai_dora)} metric=${JSON.stringify(detail?.metric)}`);
165
208
 
166
209
  if (!detail.team_id) {
167
210
  console.log('[metrics-processor] No team_id provided, defaulting to "no_team"');
@@ -179,21 +222,27 @@ export async function handler(event: EventBridgeEvent): Promise<void> {
179
222
  publishCloudWatchMetrics(detailType, detail),
180
223
  ]);
181
224
 
225
+ const labels = ['writeEventToDynamo', 'writeMetadataToDynamo', 'publishCloudWatchMetrics'];
182
226
  results.forEach((result, idx) => {
183
- const labels = ['writeEventToDynamo', 'writeMetadataToDynamo', 'publishCloudWatchMetrics'];
184
- if (result.status === 'fulfilled') {
185
- console.log(`[metrics-processor] ${labels[idx]} succeeded`);
186
- } else {
227
+ if (result.status === 'rejected') {
187
228
  console.error(`[metrics-processor] ${labels[idx]} FAILED:`, result.reason);
188
229
  }
189
230
  });
190
231
 
191
- const failures = results.filter((r) => r.status === 'rejected');
192
- if (failures.length > 0) {
193
- throw new Error(`${failures.length} operation(s) failed — check logs above`);
232
+ // Only DynamoDB write failures are retryable — the events table is the
233
+ // source of truth and DDB writes are idempotent (same pk/sk overwrites).
234
+ // CloudWatch metric publishing is best-effort: retrying a whole invocation
235
+ // because PutMetricData throttled creates a self-amplifying retry storm
236
+ // (more retries -> more PutMetricData calls -> more throttling). This
237
+ // exact loop ran at ~108M invocations/day in July 2026 and wrote 445 GB
238
+ // of logs before stopping. Metrics lost to throttling are an acceptable
239
+ // gap; events lost from DDB are not.
240
+ const ddbFailures = results.slice(0, 2).filter((r) => r.status === 'rejected');
241
+ if (ddbFailures.length > 0) {
242
+ throw new Error(`${ddbFailures.length} DynamoDB write(s) failed — retrying event`);
194
243
  }
195
244
 
196
- console.log(`[metrics-processor] Successfully processed ${detailType} for ${detail.team_id}/${detail.repo}`);
245
+ console.log(`[metrics-processor] Processed ${detailType} for ${detail.team_id}/${detail.repo}`);
197
246
  }
198
247
 
199
248
  // ---- DynamoDB events ----
@@ -211,22 +260,42 @@ async function writeEventToDynamo(
211
260
  prism_level: detail.prism_level ?? '1',
212
261
  };
213
262
 
214
- if (detail.metric) {
215
- data.metric = detail.metric;
216
- }
217
- if (detail.ai_context) {
218
- data.ai_context = detail.ai_context;
263
+ // Persist every payload section — the events table is the source of truth
264
+ // for DDB-backed dashboard panels and replay. (Previously only the first
265
+ // four were stored, silently dropping eval/guardrail/mcp/agent/security
266
+ // payloads from the durable record.)
267
+ // Shape validation: logs field drift instead of letting it vanish. See
268
+ // event-schema.ts for the failures that motivated this.
269
+ for (const w of validateEventShape(detail as unknown as Record<string, unknown>)) {
270
+ console.warn(`[schema] ${w.message}`);
219
271
  }
220
- if (detail.dora) {
221
- data.dora = detail.dora;
222
- }
223
- if (detail.ai_dora) {
224
- data.ai_dora = detail.ai_dora;
272
+
273
+ const sections = PERSISTED_SECTIONS;
274
+ for (const key of sections) {
275
+ if ((detail as Record<string, unknown>)[key]) {
276
+ data[key] = (detail as Record<string, unknown>)[key];
277
+ }
225
278
  }
226
279
 
280
+ // Sort key. Events that fan out PER FINDING (security findings, and one
281
+ // remediation per finding resolved by a merged PR) all carry the SAME
282
+ // timestamp — the scan time or the PR merge time. With sk = timestamp alone
283
+ // they collide on (pk, sk) and silently overwrite each other: a PR fixing
284
+ // three findings stored exactly one remediation event. Appending the
285
+ // finding id disambiguates them while preserving the timestamp prefix, so
286
+ // `sk BETWEEN :from AND :to` range queries and lexicographic time ordering
287
+ // still work. Follows the existing `SPAN#${ts}#${spanId}` convention in
288
+ // otel-receiver.ts. Pre-existing items keep bare-timestamp sks; readers
289
+ // split on '#', which is a no-op for those.
290
+ const findingDiscriminator =
291
+ detail.security_agent_finding?.finding_id ?? detail.security_remediation?.finding_id;
292
+ const sortKey = findingDiscriminator
293
+ ? `${detail.timestamp}#${findingDiscriminator}`
294
+ : detail.timestamp;
295
+
227
296
  const item: Record<string, { S?: string; N?: string }> = {
228
297
  pk: { S: `${detail.team_id}#${detail.repo}` },
229
- sk: { S: detail.timestamp },
298
+ sk: { S: sortKey },
230
299
  detail_type: { S: detailType },
231
300
  data: { S: JSON.stringify(data) },
232
301
  ttl: { N: ttl.toString() },
@@ -328,9 +397,6 @@ async function publishCloudWatchMetrics(
328
397
  detailType: string,
329
398
  detail: MetricDetail,
330
399
  ): Promise<void> {
331
- console.log(`[publishCloudWatchMetrics] Starting for ${detailType}, namespace=${METRIC_NAMESPACE}`);
332
- console.log(`[publishCloudWatchMetrics] dora fields: deployment_frequency=${detail.dora?.deployment_frequency} lead_time_seconds=${detail.dora?.lead_time_seconds} change_failure_rate=${detail.dora?.change_failure_rate} mttr_seconds=${detail.dora?.mttr_seconds}`);
333
- console.log(`[publishCloudWatchMetrics] ai_dora fields: ai_acceptance_rate=${detail.ai_dora?.ai_acceptance_rate} ai_to_merge_ratio=${detail.ai_dora?.ai_to_merge_ratio} eval_gate_pass_rate=${detail.ai_dora?.eval_gate_pass_rate}`);
334
400
 
335
401
  const sharedDimensions = [
336
402
  { Name: 'TeamId', Value: detail.team_id },
@@ -430,17 +496,37 @@ async function publishCloudWatchMetrics(
430
496
  Timestamp: metricTimestamp,
431
497
  });
432
498
  }
499
+ // Change-failure NUMERATOR as a counter. The CFR alarm divides this by
500
+ // DeploymentFrequency via metric math, so the ratio is evaluated over the
501
+ // alarm's own window instead of a window baked in at emit time. Emitted on
502
+ // every merged PR (0 or 1) so the alarm sees a real zero during healthy
503
+ // periods rather than sitting in INSUFFICIENT_DATA.
504
+ if (detail.dora.is_failure_fix != null) {
505
+ metricData.push({
506
+ MetricName: 'ChangeFailureCount',
507
+ Value: detail.dora.is_failure_fix ? 1 : 0,
508
+ Unit: StandardUnit.Count,
509
+ Dimensions: sharedDimensions,
510
+ Timestamp: metricTimestamp,
511
+ });
512
+ }
433
513
  }
434
514
 
435
515
  // AI-DORA metrics — scale 0–1 ratios to 0–100 for CloudWatch Percent unit
436
516
  if (detail.ai_dora) {
517
+ // AIAcceptanceRate and AIToMergeRatio were removed: neither had any
518
+ // consumer. No widget read them, and the dashboards' AI-vs-human graphs are
519
+ // driven by attribution metrics (AICommits / MergedAICommits / ...) via
520
+ // metric math instead. AIAcceptanceRate was additionally unsalvageable —
521
+ // gated on a trailer-derived ai_ratio, and defaulting to 100% whenever a PR
522
+ // had no reviews, which rewarded skipping review.
437
523
  const aiDoraMap: Array<[string, number | null, StandardUnit, boolean]> = [
438
- ['AIAcceptanceRate', detail.ai_dora.ai_acceptance_rate, StandardUnit.Percent, true],
439
- ['AIToMergeRatio', detail.ai_dora.ai_to_merge_ratio, StandardUnit.Percent, true],
440
- ['SpecToCodeHours', detail.ai_dora.spec_to_code_hours, StandardUnit.Count, false],
441
- ['PostMergeDefectRate', detail.ai_dora.post_merge_defect_rate, StandardUnit.Percent, true],
524
+ // EvalGatePassRate is the only survivor: it has a live producer
525
+ // (prism-agent-eval.yml) and a live consumer (the eval-gate alarm).
526
+ // SpecToCodeHours and PostMergeDefectRate were emitted by the
527
+ // spec-to-code-calculator and defect-correlator Lambdas, both now deleted
528
+ // as permanent no-ops. AITestCoverageDelta never had a producer at all.
442
529
  ['EvalGatePassRate', detail.ai_dora.eval_gate_pass_rate, StandardUnit.Percent, true],
443
- ['AITestCoverageDelta', detail.ai_dora.ai_test_coverage_delta, StandardUnit.Percent, true],
444
530
  ];
445
531
 
446
532
  // Token & cost metrics come from git-trailer PR sums by default. When the
@@ -691,6 +777,13 @@ async function publishCloudWatchMetrics(
691
777
  // Security remediation metrics
692
778
  if (detail.security_remediation) {
693
779
  const remediation = detail.security_remediation;
780
+ // NOTE: the AIOrigin dimension here is trailer-derived and therefore
781
+ // legacy — dashboards resolve "who fixed it" from fix_commit_shas via the
782
+ // deferred attribution join instead. It is kept, and kept UNCONDITIONAL,
783
+ // on purpose: emitting this metric sometimes with AIOrigin and sometimes
784
+ // without would create a partial dimension set, which is exactly the
785
+ // failure that left three CISO widgets permanently empty. Full set or
786
+ // nothing. The dimensionless copy below is what the SLA panels read.
694
787
  metricData.push({
695
788
  MetricName: 'SecurityRemediationTimeHours',
696
789
  Value: remediation.remediation_time_hours,
@@ -721,15 +814,11 @@ async function publishCloudWatchMetrics(
721
814
  }
722
815
 
723
816
  console.log(`[publishCloudWatchMetrics] Publishing ${metricData.length} metric data points`);
724
- metricData.forEach((m, i) => {
725
- console.log(`[publishCloudWatchMetrics] [${i}] ${m.MetricName}=${m.Value} unit=${m.Unit} dims=${JSON.stringify(m.Dimensions)}`);
726
- });
727
817
 
728
818
  // CloudWatch accepts max 1000 metric data points per call; batch in chunks of 25
729
819
  const batchSize = 25;
730
820
  for (let i = 0; i < metricData.length; i += batchSize) {
731
821
  const batch = metricData.slice(i, i + batchSize);
732
- console.log(`[publishCloudWatchMetrics] Sending batch ${Math.floor(i / batchSize) + 1} with ${batch.length} metrics`);
733
822
  try {
734
823
  await cloudwatchClient.send(
735
824
  new PutMetricDataCommand({
@@ -737,7 +826,6 @@ async function publishCloudWatchMetrics(
737
826
  MetricData: batch,
738
827
  }),
739
828
  );
740
- console.log(`[publishCloudWatchMetrics] Batch ${Math.floor(i / batchSize) + 1} sent successfully`);
741
829
  } catch (err) {
742
830
  console.error(`[publishCloudWatchMetrics] Batch ${Math.floor(i / batchSize) + 1} FAILED:`, err);
743
831
  throw err;