@weareikko/code-review 0.8.6 → 0.9.1

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.
@@ -560,7 +560,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
560
560
  return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
561
561
  }
562
562
  function buildReviewedCommitFooter(commitSha) {
563
- return `Reviewed by ${PRODUCT_LINK} v0.8.6 for commit ${commitSha}.`;
563
+ return `Reviewed by ${PRODUCT_LINK} v0.9.1 for commit ${commitSha}.`;
564
564
  }
565
565
  function extractReviewedCommitSha(body) {
566
566
  return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
@@ -1359,6 +1359,7 @@ function createDiagnosticContext(phase, config, runId, overrides = {}) {
1359
1359
  project: config.project,
1360
1360
  mr: config.mr,
1361
1361
  gitlabUrl: config.gitlabUrl,
1362
+ platform: config.platform,
1362
1363
  cwd: config.cwd,
1363
1364
  model: config.model,
1364
1365
  minSeverity: config.minSeverity,
@@ -4354,54 +4355,59 @@ async function runReview(config, options) {
4354
4355
  attachTelemetry: options.attachTelemetry,
4355
4356
  verifyStaged: diskMode ? retrievableSkipped : void 0
4356
4357
  };
4358
+ const buildUsage = () => ({
4359
+ model: config.model,
4360
+ thinkingLevel: config.thinkingLevel,
4361
+ tokens: aggregated.tokens,
4362
+ cost: aggregated.cost,
4363
+ byModel: buildByModelUsage(aggregated),
4364
+ skills: context.skills.map((s) => s.name),
4365
+ sizeNotice
4366
+ });
4357
4367
  let outputText;
4358
- if (config.reviewDepth === "full") {
4359
- const { findings, summary } = await runMultiAngleFind(context, minSeverity, userPrompt, deps);
4360
- outputText = await verifyAndSynthesize(findings, summary, diff, options.commitLog, deps);
4361
- } else {
4362
- const findAgent = createAgent({
4363
- systemPrompt,
4364
- model: primary.model,
4365
- tools,
4366
- thinkingLevel: config.thinkingLevel,
4367
- getApiKey: primary.getApiKey
4368
- });
4369
- const detachTelemetry = options.attachTelemetry?.(findAgent);
4370
- let turnCount = 0;
4371
- let toolCallCount = 0;
4372
- let finalText;
4373
- try {
4374
- finalText = await runAgentToCompletion(findAgent, userPrompt, {
4375
- timeoutMs,
4376
- onAssistantMessage: (message) => accumulateUsage(aggregated, message, primary.id),
4377
- onTurnStart: (turn) => {
4378
- turnCount = turn;
4379
- logger.debug(`Turn ${turn} started`);
4380
- },
4381
- onToolStart: (toolName, args) => {
4382
- toolCallCount += 1;
4383
- logger.debug(` → ${toolName}${formatToolArgs(toolName, args)}`);
4384
- }
4368
+ try {
4369
+ if (config.reviewDepth === "full") {
4370
+ const { findings, summary } = await runMultiAngleFind(context, minSeverity, userPrompt, deps);
4371
+ outputText = await verifyAndSynthesize(findings, summary, diff, options.commitLog, deps);
4372
+ } else {
4373
+ const findAgent = createAgent({
4374
+ systemPrompt,
4375
+ model: primary.model,
4376
+ tools,
4377
+ thinkingLevel: config.thinkingLevel,
4378
+ getApiKey: primary.getApiKey
4385
4379
  });
4386
- } finally {
4387
- detachTelemetry?.();
4380
+ const detachTelemetry = options.attachTelemetry?.(findAgent);
4381
+ let turnCount = 0;
4382
+ let toolCallCount = 0;
4383
+ let finalText;
4384
+ try {
4385
+ finalText = await runAgentToCompletion(findAgent, userPrompt, {
4386
+ timeoutMs,
4387
+ onAssistantMessage: (message) => accumulateUsage(aggregated, message, primary.id),
4388
+ onTurnStart: (turn) => {
4389
+ turnCount = turn;
4390
+ logger.debug(`Turn ${turn} started`);
4391
+ },
4392
+ onToolStart: (toolName, args) => {
4393
+ toolCallCount += 1;
4394
+ logger.debug(` → ${toolName}${formatToolArgs(toolName, args)}`);
4395
+ }
4396
+ });
4397
+ } finally {
4398
+ detachTelemetry?.();
4399
+ }
4400
+ logger.debug(`Agent finished: ${turnCount} turn(s), ${toolCallCount} tool call(s)`);
4401
+ outputText = config.reviewDepth === "verify" ? await runVerifyStage(finalText, diff, options.commitLog, deps) : finalText;
4388
4402
  }
4389
- logger.debug(`Agent finished: ${turnCount} turn(s), ${toolCallCount} tool call(s)`);
4390
- outputText = config.reviewDepth === "verify" ? await runVerifyStage(finalText, diff, options.commitLog, deps) : finalText;
4403
+ } finally {
4404
+ options.onUsage?.(buildUsage());
4391
4405
  }
4392
4406
  const reviewPath = resolve(cwd, config.reviewFile);
4393
4407
  await mkdir(dirname(reviewPath), { recursive: true });
4394
4408
  await writeFile(reviewPath, outputText, "utf8");
4395
4409
  if (retrievableSkipped.length > 0) await cleanupSkippedDiffs(cwd);
4396
- return {
4397
- model: config.model,
4398
- thinkingLevel: config.thinkingLevel,
4399
- tokens: aggregated.tokens,
4400
- cost: aggregated.cost,
4401
- byModel: buildByModelUsage(aggregated),
4402
- skills: context.skills.map((s) => s.name),
4403
- sizeNotice
4404
- };
4410
+ return buildUsage();
4405
4411
  }
4406
4412
  /**
4407
4413
  * Convert the per-model usage buckets into the public {@link ModelUsage} array,
@@ -4758,13 +4764,13 @@ async function startOtelBridge(options = {}) {
4758
4764
  unit: "{token}",
4759
4765
  advice: { explicitBucketBoundaries: TOKEN_BUCKETS }
4760
4766
  });
4761
- const operationCost = meter.createHistogram("gen_ai.client.cost", {
4762
- description: "GenAI operation cost in USD",
4767
+ const operationCost = meter.createHistogram("code_review_llm_cost_usd", {
4768
+ description: "LLM cost in USD per turn, by token type (non-standard extension)",
4763
4769
  unit: "{usd}",
4764
4770
  advice: { explicitBucketBoundaries: COST_BUCKETS_USD }
4765
4771
  });
4766
- const timeToFirstToken = meter.createHistogram("gen_ai.client.time_to_first_token", {
4767
- description: "Time to first token from the LLM",
4772
+ const timeToFirstToken = meter.createHistogram("gen_ai.client.operation.time_to_first_chunk", {
4773
+ description: "Time to first chunk from the LLM",
4768
4774
  unit: "s",
4769
4775
  advice: { explicitBucketBoundaries: TTFT_BUCKETS_S }
4770
4776
  });
@@ -4804,6 +4810,9 @@ async function startOtelBridge(options = {}) {
4804
4810
  ciAttrs,
4805
4811
  ciSpanAttrs,
4806
4812
  model: ctx.model,
4813
+ dryRun: ctx.dryRun,
4814
+ platform: ctx.platform,
4815
+ serverAddress: hostOf(ctx.gitlabUrl),
4807
4816
  rootSpanCtx
4808
4817
  });
4809
4818
  emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx);
@@ -4835,50 +4844,55 @@ async function startOtelBridge(options = {}) {
4835
4844
  entry.span.end();
4836
4845
  entry.closed = true;
4837
4846
  const status = resolveRunStatus(ctx, isError);
4838
- const projectPath = ciAttrs["gitlab.project_path"] ?? "";
4847
+ const projectPath = ciAttrs["vcs.repository.name"] ?? "";
4848
+ const vcs = vcsAttrs(ctx.platform, ctx.gitlabUrl);
4839
4849
  if (typeof ctx.durationMs === "number") reviewPhaseDuration.record(ctx.durationMs / 1e3, {
4840
4850
  ...REVIEW_SERVICE_ATTRS,
4841
- "gitlab.project_path": projectPath,
4842
- "gitlab_review.phase": ctx.phase,
4843
- "gitlab_review.status": status
4851
+ ...vcs,
4852
+ "vcs.repository.name": projectPath,
4853
+ "code_review.phase": ctx.phase,
4854
+ "code_review.status": status
4844
4855
  });
4845
4856
  if (ctx.phase === ROOT_PHASE) {
4846
4857
  const meta = runMeta.get(ctx.runId);
4847
- const pipelineSource = ciAttrs["gitlab.pipeline_source"] ?? "";
4858
+ const pipelineSource = ciAttrs["cicd.pipeline.source"] ?? "";
4848
4859
  const runMetricBase = {
4849
4860
  ...REVIEW_SERVICE_ATTRS,
4850
- "gitlab.project_path": projectPath,
4851
- "gitlab_review.dry_run": ctx.dryRun
4861
+ ...vcs,
4862
+ "vcs.repository.name": projectPath,
4863
+ "code_review.dry_run": ctx.dryRun,
4864
+ ...ctx.firstReview !== void 0 ? { "code_review.first_review": ctx.firstReview } : {}
4852
4865
  };
4853
4866
  const usage = meta?.usage ?? ctx.usage;
4854
4867
  const runModelAttrs = genAiModelAttrs(void 0, splitModel(usage?.model ?? "").modelId);
4855
4868
  reviewRunsTotal.add(1, {
4856
4869
  ...runMetricBase,
4857
- "gitlab.pipeline_source": pipelineSource,
4858
- "gitlab_review.status": status
4870
+ "cicd.pipeline.source": pipelineSource,
4871
+ "code_review.status": status
4859
4872
  });
4860
4873
  if (isError) reviewErrorsTotal.add(1, {
4861
4874
  ...runMetricBase,
4862
- "gitlab_review.status": status,
4875
+ "code_review.status": status,
4863
4876
  "error.type": errorTypeOf(ctx)
4864
4877
  });
4865
4878
  if (typeof ctx.durationMs === "number") reviewRunDuration.record(ctx.durationMs / 1e3, {
4866
4879
  ...runMetricBase,
4867
4880
  ...runModelAttrs,
4868
- "gitlab.pipeline_source": pipelineSource,
4869
- "gitlab_review.status": status
4881
+ "cicd.pipeline.source": pipelineSource,
4882
+ "code_review.status": status
4870
4883
  });
4871
4884
  const totalCostUsd = usage?.cost.total;
4872
4885
  if (totalCostUsd !== void 0) reviewTotalCost.record(totalCostUsd, {
4873
4886
  ...runMetricBase,
4874
4887
  ...runModelAttrs,
4875
- "gitlab_review.status": status
4888
+ "code_review.status": status
4876
4889
  });
4877
4890
  if (usage) {
4878
4891
  const tokenAttrs = {
4879
4892
  ...REVIEW_SERVICE_ATTRS,
4880
4893
  ...runModelAttrs,
4881
- "gitlab.project_path": projectPath
4894
+ ...vcs,
4895
+ "vcs.repository.name": projectPath
4882
4896
  };
4883
4897
  for (const [field, type] of [
4884
4898
  ["input", "input"],
@@ -4894,7 +4908,7 @@ async function startOtelBridge(options = {}) {
4894
4908
  if (bySeverity) {
4895
4909
  for (const [severity, count] of Object.entries(bySeverity)) if (count && count > 0) reviewCommentsTotal.add(count, {
4896
4910
  ...runMetricBase,
4897
- "gitlab_review.comment.severity": severity
4911
+ "code_review.comment.severity": severity
4898
4912
  });
4899
4913
  } else {
4900
4914
  const posted = ctx.posted ?? 0;
@@ -4940,16 +4954,17 @@ async function startOtelBridge(options = {}) {
4940
4954
  context: meta?.rootSpanCtx,
4941
4955
  attributes: {
4942
4956
  "service.name": SERVICE_NAME,
4943
- "event.name": "gitlab_review.comment",
4944
- "gitlab_review.run_id": runId,
4945
- "gitlab_review.comment.file": comment.file,
4946
- "gitlab_review.comment.line": comment.line,
4947
- "gitlab_review.comment.severity": comment.severity,
4948
- "gitlab_review.comment.is_duplicate": duplicate,
4957
+ "event.name": "code_review.comment",
4958
+ "code_review.run_id": runId,
4959
+ "code_review.comment.file": comment.file,
4960
+ "code_review.comment.line": comment.line,
4961
+ "code_review.comment.severity": comment.severity,
4962
+ "code_review.comment.is_duplicate": duplicate,
4949
4963
  ...meta && {
4950
- "gitlab.project_id": meta.project,
4951
- "gitlab.mr_iid": meta.mr,
4952
- "gitlab.server_url": meta.gitlabUrl,
4964
+ "vcs.repository.id": meta.project,
4965
+ "vcs.change.id": meta.mr,
4966
+ ...meta.platform ? { "vcs.provider.name": meta.platform } : {},
4967
+ ...meta.serverAddress ? { "server.address": meta.serverAddress } : {},
4953
4968
  ...meta.ciAttrs,
4954
4969
  ...meta.ciSpanAttrs
4955
4970
  }
@@ -4964,20 +4979,30 @@ async function startOtelBridge(options = {}) {
4964
4979
  ciAttrs,
4965
4980
  runId,
4966
4981
  configuredModel: runMeta.get(runId)?.model,
4982
+ dryRun: runMeta.get(runId)?.dryRun,
4983
+ platform: runMeta.get(runId)?.platform,
4984
+ serverAddress: runMeta.get(runId)?.serverAddress,
4967
4985
  captureContent
4968
4986
  });
4969
4987
  }
4970
4988
  };
4971
4989
  }
4972
4990
  /**
4973
- * Builds the dynamic `gen_ai.system` / `gen_ai.request.model` metric labels
4974
- * shared by the per-turn and per-phase GenAI metric emitters. Owning these in
4975
- * one place keeps the two emission sites from drifting into separate Prometheus
4976
- * series (the double-count `recordGenAiMetrics` documents).
4991
+ * Builds the dynamic `gen_ai.provider.name` / `gen_ai.request.model` metric
4992
+ * labels shared by the per-turn and per-phase GenAI metric emitters. Owning
4993
+ * these in one place keeps the two emission sites from drifting into separate
4994
+ * Prometheus series (the double-count `recordGenAiMetrics` documents).
4995
+ *
4996
+ * `gen_ai.provider.name` is the current GenAI-semconv discriminator; the
4997
+ * deprecated `gen_ai.system` is emitted alongside it during the transition so
4998
+ * backends still keyed on the old attribute keep working.
4977
4999
  */
4978
5000
  function genAiModelAttrs(provider, modelId) {
4979
5001
  return {
4980
- ...provider ? { "gen_ai.system": provider } : {},
5002
+ ...provider ? {
5003
+ "gen_ai.provider.name": provider,
5004
+ "gen_ai.system": provider
5005
+ } : {},
4981
5006
  ...modelId ? { "gen_ai.request.model": modelId } : {}
4982
5007
  };
4983
5008
  }
@@ -5033,9 +5058,9 @@ function recordTurnUsage(span, u, metricAttrs, tokenUsage, operationCost) {
5033
5058
  ...metricAttrs,
5034
5059
  "gen_ai.token.type": "cache_creation"
5035
5060
  });
5036
- span.setAttribute("gen_ai.usage.cost.input_usd", u.cost.input);
5037
- span.setAttribute("gen_ai.usage.cost.output_usd", u.cost.output);
5038
- span.setAttribute("gen_ai.usage.cost.total_usd", u.cost.total);
5061
+ span.setAttribute("code_review.cost.input_usd", u.cost.input);
5062
+ span.setAttribute("code_review.cost.output_usd", u.cost.output);
5063
+ span.setAttribute("code_review.cost.total_usd", u.cost.total);
5039
5064
  }
5040
5065
  }
5041
5066
  /**
@@ -5099,12 +5124,15 @@ function extractOutputMessages(msg) {
5099
5124
  }]);
5100
5125
  }
5101
5126
  function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToken, reviewerSpanCtx, options = {}) {
5102
- const { ciAttrs = {}, runId, configuredModel, captureContent = false } = options;
5127
+ const { ciAttrs = {}, runId, configuredModel, dryRun, platform, serverAddress, captureContent = false } = options;
5103
5128
  const configuredProvider = configuredModel ? splitModel(configuredModel).provider : void 0;
5104
5129
  const baseMetricAttrs = {
5105
5130
  "gen_ai.operation.name": "invoke_agent",
5106
5131
  ...REVIEW_SERVICE_ATTRS,
5107
- ...ciAttrs
5132
+ ...ciAttrs,
5133
+ ...dryRun !== void 0 ? { "code_review.dry_run": dryRun } : {},
5134
+ ...platform ? { "vcs.provider.name": platform } : {},
5135
+ ...serverAddress ? { "server.address": serverAddress } : {}
5108
5136
  };
5109
5137
  return (agent) => {
5110
5138
  let currentTurn;
@@ -5138,13 +5166,16 @@ function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToke
5138
5166
  ...baseMetricAttrs,
5139
5167
  ...genAiModelAttrs(provider, modelId)
5140
5168
  };
5141
- if (provider) span.setAttribute("gen_ai.system", provider);
5169
+ if (provider) {
5170
+ span.setAttribute("gen_ai.provider.name", provider);
5171
+ span.setAttribute("gen_ai.system", provider);
5172
+ }
5142
5173
  if (modelId) span.setAttribute("gen_ai.response.model", modelId);
5143
5174
  if (msg.stopReason) span.setAttribute("gen_ai.response.stop_reason", msg.stopReason);
5144
5175
  if (firstTokenMs !== void 0) {
5145
5176
  const ttftS = (firstTokenMs - startMs) / 1e3;
5146
5177
  timeToFirstToken.record(ttftS, metricAttrs);
5147
- span.setAttribute("gen_ai.client.time_to_first_token_s", ttftS);
5178
+ span.setAttribute("gen_ai.client.operation.time_to_first_chunk_s", ttftS);
5148
5179
  }
5149
5180
  if (msg.usage) recordTurnUsage(span, msg.usage, metricAttrs, tokenUsage, operationCost);
5150
5181
  if (captureContent) {
@@ -5205,6 +5236,26 @@ function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToke
5205
5236
  });
5206
5237
  };
5207
5238
  }
5239
+ /**
5240
+ * Applies our OTLP exporter env defaults, only when the caller hasn't set them
5241
+ * (explicit choices, including `none`, always win).
5242
+ *
5243
+ * Metrics temporality defaults to **delta** because code-review runs as a
5244
+ * short-lived CI job. With the OTel SDK's default cumulative temporality, each
5245
+ * ephemeral job's metric series starts mid-flight in the backend (its first
5246
+ * export already carries accumulated counts, and per-run histograms are a single
5247
+ * observation), so range aggregation of cost/token totals is unreliable —
5248
+ * `sum_over_time` over-counts and `increase` under-counts or returns nothing.
5249
+ * Delta makes each job's metrics self-contained deltas that sum correctly across
5250
+ * runs. Override with `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative`
5251
+ * for long-running hosts, and confirm your backend ingests delta (Grafana
5252
+ * Cloud/Mimir converts to cumulative or stores it natively) before relying on it.
5253
+ */
5254
+ function applyOtelExporterDefaults(env = process.env) {
5255
+ env.OTEL_METRICS_EXPORTER = env.OTEL_METRICS_EXPORTER ?? "otlp";
5256
+ env.OTEL_LOGS_EXPORTER = env.OTEL_LOGS_EXPORTER ?? "otlp";
5257
+ env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE ?? "delta";
5258
+ }
5208
5259
  async function loadDefaultRuntime() {
5209
5260
  let modules;
5210
5261
  try {
@@ -5215,10 +5266,9 @@ async function loadDefaultRuntime() {
5215
5266
  const [sdkNode, resources, semconv] = modules;
5216
5267
  const serviceResource = resources.resourceFromAttributes({
5217
5268
  [semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
5218
- [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.6"
5269
+ [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.1"
5219
5270
  });
5220
- process.env.OTEL_METRICS_EXPORTER = process.env.OTEL_METRICS_EXPORTER ?? "otlp";
5221
- process.env.OTEL_LOGS_EXPORTER = process.env.OTEL_LOGS_EXPORTER ?? "otlp";
5271
+ applyOtelExporterDefaults(process.env);
5222
5272
  const sdk = new sdkNode.NodeSDK({ resource: resources.defaultResource().merge(serviceResource) });
5223
5273
  sdk.start();
5224
5274
  return {
@@ -5237,14 +5287,14 @@ function emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx) {
5237
5287
  context: rootSpanCtx,
5238
5288
  attributes: {
5239
5289
  "service.name": SERVICE_NAME,
5240
- "event.name": "gitlab_review.started",
5241
- "gitlab.project_id": ctx.project,
5242
- "gitlab.mr_iid": ctx.mr,
5243
- "gitlab.server_url": ctx.gitlabUrl,
5290
+ "event.name": "code_review.started",
5291
+ "vcs.repository.id": ctx.project,
5292
+ "vcs.change.id": ctx.mr,
5293
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5244
5294
  ...ciAttrs,
5245
5295
  ...ciSpanAttrs,
5246
- "gitlab_review.run_id": ctx.runId,
5247
- "gitlab_review.dry_run": ctx.dryRun,
5296
+ "code_review.run_id": ctx.runId,
5297
+ "code_review.dry_run": ctx.dryRun,
5248
5298
  ...modelId !== void 0 && { "gen_ai.request.model": modelId }
5249
5299
  }
5250
5300
  });
@@ -5264,10 +5314,10 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
5264
5314
  context: meta?.rootSpanCtx,
5265
5315
  attributes: {
5266
5316
  "service.name": SERVICE_NAME,
5267
- "event.name": isError ? "gitlab_review.failed" : "gitlab_review.completed",
5268
- "gitlab.project_id": ctx.project,
5269
- "gitlab.mr_iid": ctx.mr,
5270
- "gitlab.server_url": ctx.gitlabUrl,
5317
+ "event.name": isError ? "code_review.failed" : "code_review.completed",
5318
+ "vcs.repository.id": ctx.project,
5319
+ "vcs.change.id": ctx.mr,
5320
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5271
5321
  ...meta?.ciAttrs,
5272
5322
  ...meta?.ciSpanAttrs,
5273
5323
  ...isError && {
@@ -5275,15 +5325,15 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
5275
5325
  ...ctx.errorInfo && { "error.message": ctx.errorInfo.message },
5276
5326
  ...typeof ctx.errorInfo?.status === "number" && { "http.response.status_code": ctx.errorInfo.status }
5277
5327
  },
5278
- "gitlab_review.run_id": ctx.runId,
5279
- "gitlab_review.duration_ms": ctx.durationMs ?? 0,
5280
- "gitlab_review.dry_run": ctx.dryRun,
5281
- "gitlab_review.comments.generated": ctx.generated ?? 0,
5282
- "gitlab_review.comments.new": ctx.newComments ?? 0,
5283
- "gitlab_review.comments.duplicate": ctx.duplicateComments ?? 0,
5284
- "gitlab_review.comments.posted": ctx.posted ?? 0,
5328
+ "code_review.run_id": ctx.runId,
5329
+ "code_review.duration_ms": ctx.durationMs ?? 0,
5330
+ "code_review.dry_run": ctx.dryRun,
5331
+ "code_review.comments.generated": ctx.generated ?? 0,
5332
+ "code_review.comments.new": ctx.newComments ?? 0,
5333
+ "code_review.comments.duplicate": ctx.duplicateComments ?? 0,
5334
+ "code_review.comments.posted": ctx.posted ?? 0,
5285
5335
  ...modelId !== void 0 && { "gen_ai.request.model": modelId },
5286
- ...cost !== void 0 && { "gen_ai.usage.cost.total_usd": cost },
5336
+ ...cost !== void 0 && { "code_review.cost.total_usd": cost },
5287
5337
  ...usage?.tokens.input !== void 0 && { "gen_ai.usage.input_tokens": usage.tokens.input + (usage.tokens.cacheRead ?? 0) },
5288
5338
  ...usage?.tokens.cacheRead && {
5289
5339
  "gen_ai.usage.input_tokens.cached": usage.tokens.cacheRead,
@@ -5302,39 +5352,39 @@ function spanNameFor(phase) {
5302
5352
  /** Creates the review-level OTel metric instruments on the given meter. */
5303
5353
  function createReviewInstruments(meter) {
5304
5354
  return {
5305
- reviewRunsTotal: meter.createCounter("gitlab_review_runs_total", { description: "Total number of code-review runs, labelled by terminal status" }),
5306
- reviewErrorsTotal: meter.createCounter("gitlab_review_errors_total", { description: "Total number of failed code-review runs, labelled by error type" }),
5355
+ reviewRunsTotal: meter.createCounter("code_review_runs_total", { description: "Total number of code-review runs, labelled by terminal status" }),
5356
+ reviewErrorsTotal: meter.createCounter("code_review_errors_total", { description: "Total number of failed code-review runs, labelled by error type" }),
5307
5357
  reviewLlmTokens: {
5308
- input: meter.createCounter("gitlab_review_llm_input_tokens_total", {
5358
+ input: meter.createCounter("code_review_llm_input_tokens_total", {
5309
5359
  description: "Total non-cached LLM input tokens consumed across code-review runs",
5310
5360
  unit: "{token}"
5311
5361
  }),
5312
- output: meter.createCounter("gitlab_review_llm_output_tokens_total", {
5362
+ output: meter.createCounter("code_review_llm_output_tokens_total", {
5313
5363
  description: "Total LLM output tokens generated across code-review runs",
5314
5364
  unit: "{token}"
5315
5365
  }),
5316
- cache_read: meter.createCounter("gitlab_review_llm_cache_read_tokens_total", {
5366
+ cache_read: meter.createCounter("code_review_llm_cache_read_tokens_total", {
5317
5367
  description: "Total LLM cache-read input tokens across code-review runs",
5318
5368
  unit: "{token}"
5319
5369
  }),
5320
- cache_creation: meter.createCounter("gitlab_review_llm_cache_creation_tokens_total", {
5370
+ cache_creation: meter.createCounter("code_review_llm_cache_creation_tokens_total", {
5321
5371
  description: "Total LLM cache-creation input tokens across code-review runs",
5322
5372
  unit: "{token}"
5323
5373
  })
5324
5374
  },
5325
- reviewRunDuration: meter.createHistogram("gitlab_review_run_duration_seconds", {
5375
+ reviewRunDuration: meter.createHistogram("code_review_run_duration_seconds", {
5326
5376
  description: "Duration of a complete code-review run",
5327
5377
  unit: "s",
5328
5378
  advice: { explicitBucketBoundaries: REVIEW_RUN_DURATION_BUCKETS_S }
5329
5379
  }),
5330
- reviewTotalCost: meter.createHistogram("gitlab_review_total_cost_usd", {
5380
+ reviewTotalCost: meter.createHistogram("code_review_total_cost_usd", {
5331
5381
  description: "Total LLM cost in USD for a complete code-review run",
5332
5382
  unit: "{usd}",
5333
5383
  advice: { explicitBucketBoundaries: REVIEW_TOTAL_COST_BUCKETS_USD }
5334
5384
  }),
5335
- reviewCommentsTotal: meter.createCounter("gitlab_review_comments_total", { description: "Total number of MR comments posted by code-review" }),
5336
- reviewDraftsPublishedTotal: meter.createCounter("gitlab_review_drafts_published_total", { description: "Total number of draft notes published by code-review" }),
5337
- reviewPhaseDuration: meter.createHistogram("gitlab_review_phase_duration_seconds", {
5385
+ reviewCommentsTotal: meter.createCounter("code_review_comments_total", { description: "Total number of MR comments posted by code-review" }),
5386
+ reviewDraftsPublishedTotal: meter.createCounter("code_review_drafts_published_total", { description: "Total number of draft notes published by code-review" }),
5387
+ reviewPhaseDuration: meter.createHistogram("code_review_phase_duration_seconds", {
5338
5388
  description: "Duration of individual code-review workflow phases",
5339
5389
  unit: "s",
5340
5390
  advice: { explicitBucketBoundaries: REVIEW_PHASE_DURATION_BUCKETS_S }
@@ -5358,7 +5408,7 @@ function errorTypeOf(ctx) {
5358
5408
  return base;
5359
5409
  }
5360
5410
  /**
5361
- * Derives the `gitlab_review.status` label used by review-level OTel metrics.
5411
+ * Derives the `code_review.status` label used by review-level OTel metrics.
5362
5412
  * Distinguishes timeouts (AbortError / ETIMEDOUT) from generic errors so
5363
5413
  * Grafana alerts can treat deadline-exceeded runs separately.
5364
5414
  */
@@ -5369,55 +5419,91 @@ function resolveRunStatus(ctx, isError) {
5369
5419
  return "error";
5370
5420
  }
5371
5421
  /**
5372
- * Extracts GitLab CI environment variables that add project/pipeline context
5373
- * to every metric, span, and log record. Only populated when running inside a
5374
- * GitLab CI pipeline; callers spread the result so missing vars add nothing.
5422
+ * Extracts low-cardinality CI project/pipeline context (repository, owner, base
5423
+ * branch, pipeline trigger) as platform-neutral `vcs.*` / `cicd.*` attributes
5424
+ * added to every metric, span, and log record. Sourced from GitLab CI or GitHub
5425
+ * Actions variables — a run is one or the other, so `??` picks whichever is set;
5426
+ * callers spread the result so missing vars add nothing.
5375
5427
  */
5376
5428
  function buildCiAttrs(env) {
5377
5429
  const attrs = {};
5378
- if (env.CI_PROJECT_PATH) attrs["gitlab.project_path"] = env.CI_PROJECT_PATH;
5379
- if (env.CI_PROJECT_NAMESPACE) attrs["gitlab.project_namespace"] = env.CI_PROJECT_NAMESPACE;
5380
- if (env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME) attrs["gitlab.mr_target_branch"] = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
5381
- if (env.CI_PIPELINE_SOURCE) attrs["gitlab.pipeline_source"] = env.CI_PIPELINE_SOURCE;
5430
+ const repository = env.CI_PROJECT_PATH ?? env.GITHUB_REPOSITORY;
5431
+ if (repository) attrs["vcs.repository.name"] = repository;
5432
+ const owner = env.CI_PROJECT_NAMESPACE ?? env.GITHUB_REPOSITORY_OWNER;
5433
+ if (owner) attrs["vcs.owner.name"] = owner;
5434
+ const baseRef = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME ?? env.GITHUB_BASE_REF;
5435
+ if (baseRef) attrs["vcs.ref.base.name"] = baseRef;
5436
+ const pipelineSource = env.CI_PIPELINE_SOURCE ?? env.GITHUB_EVENT_NAME;
5437
+ if (pipelineSource) attrs["cicd.pipeline.source"] = pipelineSource;
5382
5438
  return attrs;
5383
5439
  }
5384
5440
  /**
5385
- * Extracts high-cardinality GitLab CI identifiers that should appear on spans
5386
- * and log records but NOT on metric data points (to avoid label explosion in
5387
- * Prometheus/Mimir). Spread results via `ciSpanAttrs` stored in RunMeta.
5441
+ * Extracts high-cardinality CI identifiers that should appear on spans and log
5442
+ * records but NOT on metric data points (to avoid label explosion in
5443
+ * Prometheus/Mimir). Sourced from GitLab CI or GitHub Actions. GitHub exposes no
5444
+ * per-job run id in the environment, so the job's config-key (`GITHUB_JOB`) is
5445
+ * used for the task identifier. Spread via `ciSpanAttrs` stored in RunMeta.
5388
5446
  */
5389
5447
  function buildCiSpanAttrs(env) {
5390
5448
  const attrs = {};
5391
- if (env.CI_JOB_ID) attrs["gitlab.ci_job_id"] = env.CI_JOB_ID;
5392
- if (env.CI_PIPELINE_ID) attrs["gitlab.ci_pipeline_id"] = env.CI_PIPELINE_ID;
5449
+ const taskRunId = env.CI_JOB_ID ?? env.GITHUB_JOB;
5450
+ if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
5451
+ const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
5452
+ if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
5453
+ const repositoryUrl = env.CI_PROJECT_URL ?? (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0);
5454
+ if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
5393
5455
  return attrs;
5394
5456
  }
5457
+ /** Host of a server URL, for the low-cardinality `server.address` instance label. */
5458
+ function hostOf(url) {
5459
+ if (!url) return void 0;
5460
+ try {
5461
+ return new URL(url).host;
5462
+ } catch {
5463
+ return;
5464
+ }
5465
+ }
5466
+ /**
5467
+ * Low-cardinality VCS discriminators added to every metric, span, and log:
5468
+ * `vcs.provider.name` (gitlab | github) to filter by platform, and
5469
+ * `server.address` (the instance host) to distinguish multiple GitLab/GitHub
5470
+ * instances (e.g. gitlab.com vs a self-hosted gitlab.example.com).
5471
+ */
5472
+ function vcsAttrs(platform, serverUrl) {
5473
+ const host = hostOf(serverUrl);
5474
+ return {
5475
+ ...platform ? { "vcs.provider.name": platform } : {},
5476
+ ...host ? { "server.address": host } : {}
5477
+ };
5478
+ }
5395
5479
  function baseAttributes(ctx) {
5480
+ const host = hostOf(ctx.gitlabUrl);
5396
5481
  return {
5397
- "gitlab_review.run_id": ctx.runId,
5482
+ "code_review.run_id": ctx.runId,
5398
5483
  "gen_ai.conversation.id": ctx.runId,
5399
- "gitlab_review.phase": ctx.phase,
5400
- "gitlab.project_id": ctx.project,
5401
- "gitlab.mr_iid": ctx.mr,
5402
- "gitlab.server_url": ctx.gitlabUrl,
5403
- "gitlab_review.dry_run": ctx.dryRun,
5404
- "gitlab_review.no_post": ctx.noPost,
5405
- "gitlab_review.min_severity": ctx.minSeverity
5484
+ "code_review.phase": ctx.phase,
5485
+ "vcs.repository.id": ctx.project,
5486
+ "vcs.change.id": ctx.mr,
5487
+ ...ctx.platform ? { "vcs.provider.name": ctx.platform } : {},
5488
+ ...host ? { "server.address": host } : {},
5489
+ "code_review.dry_run": ctx.dryRun,
5490
+ "code_review.no_post": ctx.noPost,
5491
+ "code_review.min_severity": ctx.minSeverity
5406
5492
  };
5407
5493
  }
5408
5494
  var NUMERIC_RESULT_ATTRIBUTES = [
5409
- ["durationMs", "gitlab_review.duration_ms"],
5410
- ["generated", "gitlab_review.comments.generated"],
5411
- ["newComments", "gitlab_review.comments.new"],
5412
- ["duplicateComments", "gitlab_review.comments.duplicate"],
5413
- ["posted", "gitlab_review.comments.posted"],
5414
- ["draftsPublished", "gitlab_review.drafts.published"],
5415
- ["draftsCreated", "gitlab_review.drafts.created"],
5416
- ["summaryNoteId", "gitlab_review.summary.note_id"],
5417
- ["warnings", "gitlab_review.warnings"],
5418
- ["draftsAbandoned", "gitlab_review.drafts.abandoned"],
5419
- ["draftsDeletedPrePublish", "gitlab_review.drafts.deleted_pre_publish"],
5420
- ["draftsPublishFailed", "gitlab_review.drafts.publish_failed"],
5495
+ ["durationMs", "code_review.duration_ms"],
5496
+ ["generated", "code_review.comments.generated"],
5497
+ ["newComments", "code_review.comments.new"],
5498
+ ["duplicateComments", "code_review.comments.duplicate"],
5499
+ ["posted", "code_review.comments.posted"],
5500
+ ["draftsPublished", "code_review.drafts.published"],
5501
+ ["draftsCreated", "code_review.drafts.created"],
5502
+ ["summaryNoteId", "code_review.summary.note_id"],
5503
+ ["warnings", "code_review.warnings"],
5504
+ ["draftsAbandoned", "code_review.drafts.abandoned"],
5505
+ ["draftsDeletedPrePublish", "code_review.drafts.deleted_pre_publish"],
5506
+ ["draftsPublishFailed", "code_review.drafts.publish_failed"],
5421
5507
  ["diffFilesChanged", "diff.files_changed"],
5422
5508
  ["diffLinesAdded", "diff.lines_added"],
5423
5509
  ["diffLinesRemoved", "diff.lines_removed"],
@@ -5425,7 +5511,7 @@ var NUMERIC_RESULT_ATTRIBUTES = [
5425
5511
  ["httpResponseBodySize", "http.response.body.size"]
5426
5512
  ];
5427
5513
  var STRING_RESULT_ATTRIBUTES = [
5428
- ["summaryAction", "gitlab_review.summary.action"],
5514
+ ["summaryAction", "code_review.summary.action"],
5429
5515
  ["httpRequestMethod", "http.request.method"],
5430
5516
  ["httpUrl", "url.full"],
5431
5517
  ["serverAddress", "server.address"]
@@ -5442,7 +5528,10 @@ function applyResultAttributes(span, ctx) {
5442
5528
  }
5443
5529
  function applyGenAiAttributes(span, ctx) {
5444
5530
  const { provider, modelId } = splitModel(ctx.model ?? "");
5445
- if (provider) span.setAttribute("gen_ai.system", provider);
5531
+ if (provider) {
5532
+ span.setAttribute("gen_ai.provider.name", provider);
5533
+ span.setAttribute("gen_ai.system", provider);
5534
+ }
5446
5535
  if (modelId) {
5447
5536
  span.setAttribute("gen_ai.request.model", modelId);
5448
5537
  span.setAttribute("gen_ai.response.model", modelId);
@@ -5452,16 +5541,16 @@ function applyGenAiAttributes(span, ctx) {
5452
5541
  const usage = ctx.usage;
5453
5542
  if (!usage) return;
5454
5543
  setTokenUsageSpanAttributes(span, usage.tokens);
5455
- span.setAttribute("gen_ai.usage.cost.input_usd", usage.cost.input);
5456
- span.setAttribute("gen_ai.usage.cost.output_usd", usage.cost.output);
5457
- span.setAttribute("gen_ai.usage.cost.cache_read_usd", usage.cost.cacheRead);
5458
- span.setAttribute("gen_ai.usage.cost.cache_creation_usd", usage.cost.cacheWrite);
5459
- span.setAttribute("gen_ai.usage.cost.total_usd", usage.cost.total);
5544
+ span.setAttribute("code_review.cost.input_usd", usage.cost.input);
5545
+ span.setAttribute("code_review.cost.output_usd", usage.cost.output);
5546
+ span.setAttribute("code_review.cost.cache_read_usd", usage.cost.cacheRead);
5547
+ span.setAttribute("code_review.cost.cache_creation_usd", usage.cost.cacheWrite);
5548
+ span.setAttribute("code_review.cost.total_usd", usage.cost.total);
5460
5549
  }
5461
5550
  /**
5462
5551
  * Records `gen_ai.client.operation.duration` for the `reviewer.run` phase.
5463
5552
  *
5464
- * Token usage (`gen_ai.client.token.usage`) and cost (`gen_ai.client.cost`) are
5553
+ * Token usage (`gen_ai.client.token.usage`) and cost (`code_review_llm_cost_usd`) are
5465
5554
  * intentionally NOT recorded here. They are emitted per-turn by
5466
5555
  * `buildAgentSubscriber` from the live agent event stream. Keeping a single
5467
5556
  * emission point for each metric prevents the double-count that previously
@@ -5474,7 +5563,9 @@ function recordGenAiMetrics(durationHist, ctx, isError, ciAttrs = {}) {
5474
5563
  "gen_ai.operation.name": "invoke_agent",
5475
5564
  ...REVIEW_SERVICE_ATTRS,
5476
5565
  ...ciAttrs,
5477
- ...genAiModelAttrs(provider, modelId)
5566
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5567
+ ...genAiModelAttrs(provider, modelId),
5568
+ "code_review.dry_run": ctx.dryRun
5478
5569
  };
5479
5570
  if (isError) attrs["error.type"] = errorTypeOf(ctx);
5480
5571
  if (typeof ctx.durationMs === "number") durationHist.record(ctx.durationMs / 1e3, attrs);
@@ -5647,7 +5738,7 @@ function boldCommentTitle(body) {
5647
5738
  */
5648
5739
  function buildCommentBody(body, commitSha, confidence) {
5649
5740
  const confidenceLine = `_Confidence: ${confidence}._`;
5650
- const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.6 for commit ${commitSha}.</sub>`;
5741
+ const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.1 for commit ${commitSha}.</sub>`;
5651
5742
  return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
5652
5743
  }
5653
5744
  function buildPayload(comment, body, refs, resolved) {
@@ -6442,6 +6533,7 @@ async function run(config, bridges) {
6442
6533
  const refs = await tracedRead("scm.get_latest_version", () => platform.getRefs());
6443
6534
  const initialDiscussions = await tracedRead("scm.get_discussions", () => platform.getDiscussions());
6444
6535
  const reviewedCommitSha = findExistingReviewedCommitSha(initialDiscussions);
6536
+ runContext.firstReview = findExistingSummaryNote(initialDiscussions) === null;
6445
6537
  if (!config.forceReview && !config.dryRun && !config.noPost && reviewedCommitSha === refs.head_sha) {
6446
6538
  const usage = zeroReviewUsage(config.model, config.thinkingLevel);
6447
6539
  runContext.usage = usage;
@@ -6484,27 +6576,36 @@ async function run(config, bridges) {
6484
6576
  if (priorThreads.length > 0) logger.info(`Found ${priorThreads.length} prior thread(s) with developer replies — including as context.`);
6485
6577
  logger.info("Running review...");
6486
6578
  let usage;
6579
+ let partialUsage;
6487
6580
  try {
6488
6581
  usage = await traceDiagnosticPhase("reviewer.run", config, runId, async (context) => {
6489
- const result = await runReview(config, {
6490
- cwd: config.cwd,
6491
- diff,
6492
- commitLog,
6493
- priorThreads,
6494
- intent: {
6495
- title: mr.title,
6496
- description: mr.description
6497
- },
6498
- logger,
6499
- attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
6500
- sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
6501
- });
6502
- context.usage = result;
6503
- return result;
6582
+ try {
6583
+ const result = await runReview(config, {
6584
+ cwd: config.cwd,
6585
+ diff,
6586
+ commitLog,
6587
+ priorThreads,
6588
+ intent: {
6589
+ title: mr.title,
6590
+ description: mr.description
6591
+ },
6592
+ logger,
6593
+ attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
6594
+ onUsage: (u) => {
6595
+ partialUsage = u;
6596
+ },
6597
+ sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
6598
+ });
6599
+ context.usage = result;
6600
+ return result;
6601
+ } catch (error) {
6602
+ if (partialUsage) context.usage = partialUsage;
6603
+ throw error;
6604
+ }
6504
6605
  });
6505
6606
  } catch (error) {
6506
6607
  if (!isQuotaExceededError(error)) throw error;
6507
- const skipUsage = zeroReviewUsage(config.model, config.thinkingLevel);
6608
+ const skipUsage = partialUsage ?? zeroReviewUsage(config.model, config.thinkingLevel);
6508
6609
  runContext.usage = skipUsage;
6509
6610
  runContext.generated = 0;
6510
6611
  runContext.newComments = 0;
@@ -6744,10 +6845,10 @@ async function main(argv = process.argv.slice(2)) {
6744
6845
  return;
6745
6846
  }
6746
6847
  if (argv.includes("--version") || argv.includes("-v")) {
6747
- console.log("0.8.6");
6848
+ console.log("0.9.1");
6748
6849
  return;
6749
6850
  }
6750
- process.stderr.write(`[code-review] @weareikko/code-review v0.8.6\n`);
6851
+ process.stderr.write(`[code-review] @weareikko/code-review v0.9.1\n`);
6751
6852
  assertNodeVersion();
6752
6853
  applyCodeReviewEnvPrefix();
6753
6854
  applyDefaultCacheRetention();
@@ -6770,4 +6871,4 @@ if (isDirectRun()) main().catch((error) => {
6770
6871
  //#endregion
6771
6872
  export { normalizeBody as $, SUMMARY_HISTORY_END as A, buildSummaryHistoryEntries as B, createDiagnosticContext as C, traceDiagnosticPhase as D, traceDiagnostic as E, SUMMARY_MARKER as F, findExistingSummaryNoteId as G, extractSummaryHistoryEntries as H, buildArchivedSummaryEntry as I, upsertSummaryNote as J, stripSummaryHistory as K, buildReviewedCommitFooter as L, SUMMARY_HISTORY_ENTRY_START as M, SUMMARY_HISTORY_LIMIT as N, normalizeSeverity as O, SUMMARY_HISTORY_START as P, fingerprints as Q, buildSizeNoticeBlock as R, DIAGNOSTIC_CHANNEL_PREFIX as S, diagnosticChannels as T, findExistingReviewedCommitSha as U, extractReviewedCommitSha as V, findExistingSummaryNote as W, extractDiffHunkContext as X, appendFingerprintMarkers as Y, extractExistingFingerprints as Z, resolveNpmSkillDir as _, main as a, parseReviewMarkdownWithWarnings as b, buildGeneratedComments as c, startOtelBridge as d, sha256 as et, filterDiff as f, parseSkillSpec as g, loadNamedSkill as h, formatUsageLine as i, SUMMARY_HISTORY_ENTRY_END as j, toGitLabReviewSeverity as k, buildPayload as l, gitSkillCacheKey as m, formatPerModelUsage as n, run as o, runReview as p, stripSummaryMarker as q, formatSkillsFooter as r, withHttpStamping as s, countPostedBySeverity as t, isOtelEnabled as u, resolveSkillCacheDir as v, createDiagnosticRunId as w, DIAGNOSTIC_CHANNEL_NAMES as x, parseReviewMarkdown as y, buildSummaryBody as z };
6772
6873
 
6773
- //# sourceMappingURL=cli-BbR1oftT.js.map
6874
+ //# sourceMappingURL=cli-BGn8g23f.js.map