@weareikko/code-review 0.8.6 → 0.9.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.
@@ -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.0 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) {
@@ -5215,7 +5246,7 @@ async function loadDefaultRuntime() {
5215
5246
  const [sdkNode, resources, semconv] = modules;
5216
5247
  const serviceResource = resources.resourceFromAttributes({
5217
5248
  [semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
5218
- [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.6"
5249
+ [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.0"
5219
5250
  });
5220
5251
  process.env.OTEL_METRICS_EXPORTER = process.env.OTEL_METRICS_EXPORTER ?? "otlp";
5221
5252
  process.env.OTEL_LOGS_EXPORTER = process.env.OTEL_LOGS_EXPORTER ?? "otlp";
@@ -5237,14 +5268,14 @@ function emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx) {
5237
5268
  context: rootSpanCtx,
5238
5269
  attributes: {
5239
5270
  "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,
5271
+ "event.name": "code_review.started",
5272
+ "vcs.repository.id": ctx.project,
5273
+ "vcs.change.id": ctx.mr,
5274
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5244
5275
  ...ciAttrs,
5245
5276
  ...ciSpanAttrs,
5246
- "gitlab_review.run_id": ctx.runId,
5247
- "gitlab_review.dry_run": ctx.dryRun,
5277
+ "code_review.run_id": ctx.runId,
5278
+ "code_review.dry_run": ctx.dryRun,
5248
5279
  ...modelId !== void 0 && { "gen_ai.request.model": modelId }
5249
5280
  }
5250
5281
  });
@@ -5264,10 +5295,10 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
5264
5295
  context: meta?.rootSpanCtx,
5265
5296
  attributes: {
5266
5297
  "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,
5298
+ "event.name": isError ? "code_review.failed" : "code_review.completed",
5299
+ "vcs.repository.id": ctx.project,
5300
+ "vcs.change.id": ctx.mr,
5301
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5271
5302
  ...meta?.ciAttrs,
5272
5303
  ...meta?.ciSpanAttrs,
5273
5304
  ...isError && {
@@ -5275,15 +5306,15 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
5275
5306
  ...ctx.errorInfo && { "error.message": ctx.errorInfo.message },
5276
5307
  ...typeof ctx.errorInfo?.status === "number" && { "http.response.status_code": ctx.errorInfo.status }
5277
5308
  },
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,
5309
+ "code_review.run_id": ctx.runId,
5310
+ "code_review.duration_ms": ctx.durationMs ?? 0,
5311
+ "code_review.dry_run": ctx.dryRun,
5312
+ "code_review.comments.generated": ctx.generated ?? 0,
5313
+ "code_review.comments.new": ctx.newComments ?? 0,
5314
+ "code_review.comments.duplicate": ctx.duplicateComments ?? 0,
5315
+ "code_review.comments.posted": ctx.posted ?? 0,
5285
5316
  ...modelId !== void 0 && { "gen_ai.request.model": modelId },
5286
- ...cost !== void 0 && { "gen_ai.usage.cost.total_usd": cost },
5317
+ ...cost !== void 0 && { "code_review.cost.total_usd": cost },
5287
5318
  ...usage?.tokens.input !== void 0 && { "gen_ai.usage.input_tokens": usage.tokens.input + (usage.tokens.cacheRead ?? 0) },
5288
5319
  ...usage?.tokens.cacheRead && {
5289
5320
  "gen_ai.usage.input_tokens.cached": usage.tokens.cacheRead,
@@ -5302,39 +5333,39 @@ function spanNameFor(phase) {
5302
5333
  /** Creates the review-level OTel metric instruments on the given meter. */
5303
5334
  function createReviewInstruments(meter) {
5304
5335
  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" }),
5336
+ reviewRunsTotal: meter.createCounter("code_review_runs_total", { description: "Total number of code-review runs, labelled by terminal status" }),
5337
+ reviewErrorsTotal: meter.createCounter("code_review_errors_total", { description: "Total number of failed code-review runs, labelled by error type" }),
5307
5338
  reviewLlmTokens: {
5308
- input: meter.createCounter("gitlab_review_llm_input_tokens_total", {
5339
+ input: meter.createCounter("code_review_llm_input_tokens_total", {
5309
5340
  description: "Total non-cached LLM input tokens consumed across code-review runs",
5310
5341
  unit: "{token}"
5311
5342
  }),
5312
- output: meter.createCounter("gitlab_review_llm_output_tokens_total", {
5343
+ output: meter.createCounter("code_review_llm_output_tokens_total", {
5313
5344
  description: "Total LLM output tokens generated across code-review runs",
5314
5345
  unit: "{token}"
5315
5346
  }),
5316
- cache_read: meter.createCounter("gitlab_review_llm_cache_read_tokens_total", {
5347
+ cache_read: meter.createCounter("code_review_llm_cache_read_tokens_total", {
5317
5348
  description: "Total LLM cache-read input tokens across code-review runs",
5318
5349
  unit: "{token}"
5319
5350
  }),
5320
- cache_creation: meter.createCounter("gitlab_review_llm_cache_creation_tokens_total", {
5351
+ cache_creation: meter.createCounter("code_review_llm_cache_creation_tokens_total", {
5321
5352
  description: "Total LLM cache-creation input tokens across code-review runs",
5322
5353
  unit: "{token}"
5323
5354
  })
5324
5355
  },
5325
- reviewRunDuration: meter.createHistogram("gitlab_review_run_duration_seconds", {
5356
+ reviewRunDuration: meter.createHistogram("code_review_run_duration_seconds", {
5326
5357
  description: "Duration of a complete code-review run",
5327
5358
  unit: "s",
5328
5359
  advice: { explicitBucketBoundaries: REVIEW_RUN_DURATION_BUCKETS_S }
5329
5360
  }),
5330
- reviewTotalCost: meter.createHistogram("gitlab_review_total_cost_usd", {
5361
+ reviewTotalCost: meter.createHistogram("code_review_total_cost_usd", {
5331
5362
  description: "Total LLM cost in USD for a complete code-review run",
5332
5363
  unit: "{usd}",
5333
5364
  advice: { explicitBucketBoundaries: REVIEW_TOTAL_COST_BUCKETS_USD }
5334
5365
  }),
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", {
5366
+ reviewCommentsTotal: meter.createCounter("code_review_comments_total", { description: "Total number of MR comments posted by code-review" }),
5367
+ reviewDraftsPublishedTotal: meter.createCounter("code_review_drafts_published_total", { description: "Total number of draft notes published by code-review" }),
5368
+ reviewPhaseDuration: meter.createHistogram("code_review_phase_duration_seconds", {
5338
5369
  description: "Duration of individual code-review workflow phases",
5339
5370
  unit: "s",
5340
5371
  advice: { explicitBucketBoundaries: REVIEW_PHASE_DURATION_BUCKETS_S }
@@ -5358,7 +5389,7 @@ function errorTypeOf(ctx) {
5358
5389
  return base;
5359
5390
  }
5360
5391
  /**
5361
- * Derives the `gitlab_review.status` label used by review-level OTel metrics.
5392
+ * Derives the `code_review.status` label used by review-level OTel metrics.
5362
5393
  * Distinguishes timeouts (AbortError / ETIMEDOUT) from generic errors so
5363
5394
  * Grafana alerts can treat deadline-exceeded runs separately.
5364
5395
  */
@@ -5369,55 +5400,91 @@ function resolveRunStatus(ctx, isError) {
5369
5400
  return "error";
5370
5401
  }
5371
5402
  /**
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.
5403
+ * Extracts low-cardinality CI project/pipeline context (repository, owner, base
5404
+ * branch, pipeline trigger) as platform-neutral `vcs.*` / `cicd.*` attributes
5405
+ * added to every metric, span, and log record. Sourced from GitLab CI or GitHub
5406
+ * Actions variables — a run is one or the other, so `??` picks whichever is set;
5407
+ * callers spread the result so missing vars add nothing.
5375
5408
  */
5376
5409
  function buildCiAttrs(env) {
5377
5410
  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;
5411
+ const repository = env.CI_PROJECT_PATH ?? env.GITHUB_REPOSITORY;
5412
+ if (repository) attrs["vcs.repository.name"] = repository;
5413
+ const owner = env.CI_PROJECT_NAMESPACE ?? env.GITHUB_REPOSITORY_OWNER;
5414
+ if (owner) attrs["vcs.owner.name"] = owner;
5415
+ const baseRef = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME ?? env.GITHUB_BASE_REF;
5416
+ if (baseRef) attrs["vcs.ref.base.name"] = baseRef;
5417
+ const pipelineSource = env.CI_PIPELINE_SOURCE ?? env.GITHUB_EVENT_NAME;
5418
+ if (pipelineSource) attrs["cicd.pipeline.source"] = pipelineSource;
5382
5419
  return attrs;
5383
5420
  }
5384
5421
  /**
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.
5422
+ * Extracts high-cardinality CI identifiers that should appear on spans and log
5423
+ * records but NOT on metric data points (to avoid label explosion in
5424
+ * Prometheus/Mimir). Sourced from GitLab CI or GitHub Actions. GitHub exposes no
5425
+ * per-job run id in the environment, so the job's config-key (`GITHUB_JOB`) is
5426
+ * used for the task identifier. Spread via `ciSpanAttrs` stored in RunMeta.
5388
5427
  */
5389
5428
  function buildCiSpanAttrs(env) {
5390
5429
  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;
5430
+ const taskRunId = env.CI_JOB_ID ?? env.GITHUB_JOB;
5431
+ if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
5432
+ const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
5433
+ if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
5434
+ const repositoryUrl = env.CI_PROJECT_URL ?? (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0);
5435
+ if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
5393
5436
  return attrs;
5394
5437
  }
5438
+ /** Host of a server URL, for the low-cardinality `server.address` instance label. */
5439
+ function hostOf(url) {
5440
+ if (!url) return void 0;
5441
+ try {
5442
+ return new URL(url).host;
5443
+ } catch {
5444
+ return;
5445
+ }
5446
+ }
5447
+ /**
5448
+ * Low-cardinality VCS discriminators added to every metric, span, and log:
5449
+ * `vcs.provider.name` (gitlab | github) to filter by platform, and
5450
+ * `server.address` (the instance host) to distinguish multiple GitLab/GitHub
5451
+ * instances (e.g. gitlab.com vs a self-hosted gitlab.example.com).
5452
+ */
5453
+ function vcsAttrs(platform, serverUrl) {
5454
+ const host = hostOf(serverUrl);
5455
+ return {
5456
+ ...platform ? { "vcs.provider.name": platform } : {},
5457
+ ...host ? { "server.address": host } : {}
5458
+ };
5459
+ }
5395
5460
  function baseAttributes(ctx) {
5461
+ const host = hostOf(ctx.gitlabUrl);
5396
5462
  return {
5397
- "gitlab_review.run_id": ctx.runId,
5463
+ "code_review.run_id": ctx.runId,
5398
5464
  "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
5465
+ "code_review.phase": ctx.phase,
5466
+ "vcs.repository.id": ctx.project,
5467
+ "vcs.change.id": ctx.mr,
5468
+ ...ctx.platform ? { "vcs.provider.name": ctx.platform } : {},
5469
+ ...host ? { "server.address": host } : {},
5470
+ "code_review.dry_run": ctx.dryRun,
5471
+ "code_review.no_post": ctx.noPost,
5472
+ "code_review.min_severity": ctx.minSeverity
5406
5473
  };
5407
5474
  }
5408
5475
  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"],
5476
+ ["durationMs", "code_review.duration_ms"],
5477
+ ["generated", "code_review.comments.generated"],
5478
+ ["newComments", "code_review.comments.new"],
5479
+ ["duplicateComments", "code_review.comments.duplicate"],
5480
+ ["posted", "code_review.comments.posted"],
5481
+ ["draftsPublished", "code_review.drafts.published"],
5482
+ ["draftsCreated", "code_review.drafts.created"],
5483
+ ["summaryNoteId", "code_review.summary.note_id"],
5484
+ ["warnings", "code_review.warnings"],
5485
+ ["draftsAbandoned", "code_review.drafts.abandoned"],
5486
+ ["draftsDeletedPrePublish", "code_review.drafts.deleted_pre_publish"],
5487
+ ["draftsPublishFailed", "code_review.drafts.publish_failed"],
5421
5488
  ["diffFilesChanged", "diff.files_changed"],
5422
5489
  ["diffLinesAdded", "diff.lines_added"],
5423
5490
  ["diffLinesRemoved", "diff.lines_removed"],
@@ -5425,7 +5492,7 @@ var NUMERIC_RESULT_ATTRIBUTES = [
5425
5492
  ["httpResponseBodySize", "http.response.body.size"]
5426
5493
  ];
5427
5494
  var STRING_RESULT_ATTRIBUTES = [
5428
- ["summaryAction", "gitlab_review.summary.action"],
5495
+ ["summaryAction", "code_review.summary.action"],
5429
5496
  ["httpRequestMethod", "http.request.method"],
5430
5497
  ["httpUrl", "url.full"],
5431
5498
  ["serverAddress", "server.address"]
@@ -5442,7 +5509,10 @@ function applyResultAttributes(span, ctx) {
5442
5509
  }
5443
5510
  function applyGenAiAttributes(span, ctx) {
5444
5511
  const { provider, modelId } = splitModel(ctx.model ?? "");
5445
- if (provider) span.setAttribute("gen_ai.system", provider);
5512
+ if (provider) {
5513
+ span.setAttribute("gen_ai.provider.name", provider);
5514
+ span.setAttribute("gen_ai.system", provider);
5515
+ }
5446
5516
  if (modelId) {
5447
5517
  span.setAttribute("gen_ai.request.model", modelId);
5448
5518
  span.setAttribute("gen_ai.response.model", modelId);
@@ -5452,16 +5522,16 @@ function applyGenAiAttributes(span, ctx) {
5452
5522
  const usage = ctx.usage;
5453
5523
  if (!usage) return;
5454
5524
  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);
5525
+ span.setAttribute("code_review.cost.input_usd", usage.cost.input);
5526
+ span.setAttribute("code_review.cost.output_usd", usage.cost.output);
5527
+ span.setAttribute("code_review.cost.cache_read_usd", usage.cost.cacheRead);
5528
+ span.setAttribute("code_review.cost.cache_creation_usd", usage.cost.cacheWrite);
5529
+ span.setAttribute("code_review.cost.total_usd", usage.cost.total);
5460
5530
  }
5461
5531
  /**
5462
5532
  * Records `gen_ai.client.operation.duration` for the `reviewer.run` phase.
5463
5533
  *
5464
- * Token usage (`gen_ai.client.token.usage`) and cost (`gen_ai.client.cost`) are
5534
+ * Token usage (`gen_ai.client.token.usage`) and cost (`code_review_llm_cost_usd`) are
5465
5535
  * intentionally NOT recorded here. They are emitted per-turn by
5466
5536
  * `buildAgentSubscriber` from the live agent event stream. Keeping a single
5467
5537
  * emission point for each metric prevents the double-count that previously
@@ -5474,7 +5544,9 @@ function recordGenAiMetrics(durationHist, ctx, isError, ciAttrs = {}) {
5474
5544
  "gen_ai.operation.name": "invoke_agent",
5475
5545
  ...REVIEW_SERVICE_ATTRS,
5476
5546
  ...ciAttrs,
5477
- ...genAiModelAttrs(provider, modelId)
5547
+ ...vcsAttrs(ctx.platform, ctx.gitlabUrl),
5548
+ ...genAiModelAttrs(provider, modelId),
5549
+ "code_review.dry_run": ctx.dryRun
5478
5550
  };
5479
5551
  if (isError) attrs["error.type"] = errorTypeOf(ctx);
5480
5552
  if (typeof ctx.durationMs === "number") durationHist.record(ctx.durationMs / 1e3, attrs);
@@ -5647,7 +5719,7 @@ function boldCommentTitle(body) {
5647
5719
  */
5648
5720
  function buildCommentBody(body, commitSha, confidence) {
5649
5721
  const confidenceLine = `_Confidence: ${confidence}._`;
5650
- const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.6 for commit ${commitSha}.</sub>`;
5722
+ const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.0 for commit ${commitSha}.</sub>`;
5651
5723
  return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
5652
5724
  }
5653
5725
  function buildPayload(comment, body, refs, resolved) {
@@ -6442,6 +6514,7 @@ async function run(config, bridges) {
6442
6514
  const refs = await tracedRead("scm.get_latest_version", () => platform.getRefs());
6443
6515
  const initialDiscussions = await tracedRead("scm.get_discussions", () => platform.getDiscussions());
6444
6516
  const reviewedCommitSha = findExistingReviewedCommitSha(initialDiscussions);
6517
+ runContext.firstReview = findExistingSummaryNote(initialDiscussions) === null;
6445
6518
  if (!config.forceReview && !config.dryRun && !config.noPost && reviewedCommitSha === refs.head_sha) {
6446
6519
  const usage = zeroReviewUsage(config.model, config.thinkingLevel);
6447
6520
  runContext.usage = usage;
@@ -6484,27 +6557,36 @@ async function run(config, bridges) {
6484
6557
  if (priorThreads.length > 0) logger.info(`Found ${priorThreads.length} prior thread(s) with developer replies — including as context.`);
6485
6558
  logger.info("Running review...");
6486
6559
  let usage;
6560
+ let partialUsage;
6487
6561
  try {
6488
6562
  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;
6563
+ try {
6564
+ const result = await runReview(config, {
6565
+ cwd: config.cwd,
6566
+ diff,
6567
+ commitLog,
6568
+ priorThreads,
6569
+ intent: {
6570
+ title: mr.title,
6571
+ description: mr.description
6572
+ },
6573
+ logger,
6574
+ attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
6575
+ onUsage: (u) => {
6576
+ partialUsage = u;
6577
+ },
6578
+ sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
6579
+ });
6580
+ context.usage = result;
6581
+ return result;
6582
+ } catch (error) {
6583
+ if (partialUsage) context.usage = partialUsage;
6584
+ throw error;
6585
+ }
6504
6586
  });
6505
6587
  } catch (error) {
6506
6588
  if (!isQuotaExceededError(error)) throw error;
6507
- const skipUsage = zeroReviewUsage(config.model, config.thinkingLevel);
6589
+ const skipUsage = partialUsage ?? zeroReviewUsage(config.model, config.thinkingLevel);
6508
6590
  runContext.usage = skipUsage;
6509
6591
  runContext.generated = 0;
6510
6592
  runContext.newComments = 0;
@@ -6744,10 +6826,10 @@ async function main(argv = process.argv.slice(2)) {
6744
6826
  return;
6745
6827
  }
6746
6828
  if (argv.includes("--version") || argv.includes("-v")) {
6747
- console.log("0.8.6");
6829
+ console.log("0.9.0");
6748
6830
  return;
6749
6831
  }
6750
- process.stderr.write(`[code-review] @weareikko/code-review v0.8.6\n`);
6832
+ process.stderr.write(`[code-review] @weareikko/code-review v0.9.0\n`);
6751
6833
  assertNodeVersion();
6752
6834
  applyCodeReviewEnvPrefix();
6753
6835
  applyDefaultCacheRetention();
@@ -6770,4 +6852,4 @@ if (isDirectRun()) main().catch((error) => {
6770
6852
  //#endregion
6771
6853
  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
6854
 
6773
- //# sourceMappingURL=cli-BbR1oftT.js.map
6855
+ //# sourceMappingURL=cli-DK0kteLL.js.map