@debugbundle/cli 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +240 -45
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -14483,6 +14483,8 @@ var CaptureBreadcrumbsValues = ["local_only", "exception_only", "standalone"];
14483
14483
  var CaptureBreadcrumbsSchema = external_exports.enum(CaptureBreadcrumbsValues);
14484
14484
  var CaptureProbeEventsValues = ["buffer_only", "standalone_when_activated"];
14485
14485
  var CaptureProbeEventsSchema = external_exports.enum(CaptureProbeEventsValues);
14486
+ var RequestSignalClassificationValues = ["incident_signal", "context_signal"];
14487
+ var RequestSignalClassificationSchema = external_exports.enum(RequestSignalClassificationValues);
14486
14488
  var CapturePolicySchema = external_exports.object({
14487
14489
  project_id: external_exports.string().uuid(),
14488
14490
  preset: CapturePresetSchema,
@@ -14499,6 +14501,55 @@ var CapturePolicyUpdateSchema = external_exports.object({
14499
14501
  capture_breadcrumbs: CaptureBreadcrumbsSchema.nullable().optional(),
14500
14502
  capture_probe_events: CaptureProbeEventsSchema.nullable().optional()
14501
14503
  });
14504
+ var BALANCED_IMMEDIATE_REQUEST_STATUSES = /* @__PURE__ */ new Set([408, 423, 424, 425, 429]);
14505
+ var INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES = /* @__PURE__ */ new Set([...BALANCED_IMMEDIATE_REQUEST_STATUSES, 409]);
14506
+ var BALANCED_STANDARD_ANOMALY_STATUSES = /* @__PURE__ */ new Set([401, 403, 404, 409, 422]);
14507
+ var BALANCED_HIGH_VOLUME_ANOMALY_STATUSES = /* @__PURE__ */ new Set([400, 410]);
14508
+ var INVESTIGATIVE_ANOMALY_STATUSES = /* @__PURE__ */ new Set([...BALANCED_STANDARD_ANOMALY_STATUSES, ...BALANCED_HIGH_VOLUME_ANOMALY_STATUSES]);
14509
+ function classifyRequestStatus(input) {
14510
+ const { responseStatus, capturePreset } = input;
14511
+ if (responseStatus === null || !Number.isFinite(responseStatus)) {
14512
+ return "context_signal";
14513
+ }
14514
+ if (responseStatus >= 500) {
14515
+ return "incident_signal";
14516
+ }
14517
+ if (capturePreset === "investigative") {
14518
+ return INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES.has(responseStatus) ? "incident_signal" : "context_signal";
14519
+ }
14520
+ if (capturePreset === "balanced") {
14521
+ return BALANCED_IMMEDIATE_REQUEST_STATUSES.has(responseStatus) ? "incident_signal" : "context_signal";
14522
+ }
14523
+ return "context_signal";
14524
+ }
14525
+ function getRequestAnomalyThreshold(input) {
14526
+ const { responseStatus, capturePreset } = input;
14527
+ if (responseStatus === null || !Number.isFinite(responseStatus) || responseStatus < 400 || responseStatus >= 500) {
14528
+ return null;
14529
+ }
14530
+ if (capturePreset === "minimal") {
14531
+ return null;
14532
+ }
14533
+ if (capturePreset === "investigative") {
14534
+ return INVESTIGATIVE_ANOMALY_STATUSES.has(responseStatus) ? {
14535
+ minimum_occurrences_5m: 8,
14536
+ minimum_ratio_5m_to_1h: 2
14537
+ } : null;
14538
+ }
14539
+ if (BALANCED_STANDARD_ANOMALY_STATUSES.has(responseStatus)) {
14540
+ return {
14541
+ minimum_occurrences_5m: 20,
14542
+ minimum_ratio_5m_to_1h: 3
14543
+ };
14544
+ }
14545
+ if (BALANCED_HIGH_VOLUME_ANOMALY_STATUSES.has(responseStatus)) {
14546
+ return {
14547
+ minimum_occurrences_5m: 50,
14548
+ minimum_ratio_5m_to_1h: 5
14549
+ };
14550
+ }
14551
+ return null;
14552
+ }
14502
14553
 
14503
14554
  // ../../packages/shared-types/src/index.ts
14504
14555
  function createUuidV4() {
@@ -15124,7 +15175,7 @@ function buildCliReference() {
15124
15175
  "- `debugbundle ingest <file> --format <format> [--json]`",
15125
15176
  "- `debugbundle watch --log <file> --format <format> [--json]`",
15126
15177
  "- `debugbundle watch --cloud --log <file> --format <format> [--json]`",
15127
- "- `debugbundle process [--json]`",
15178
+ "- `debugbundle process [--preset <minimal|balanced|investigative>] [--json]`",
15128
15179
  "",
15129
15180
  "## Investigation",
15130
15181
  "",
@@ -15670,7 +15721,7 @@ var import_node_path4 = require("node:path");
15670
15721
 
15671
15722
  // ../../packages/retrieval-client/src/index.ts
15672
15723
  var IncidentReasonSchema = external_exports.object({
15673
- kind: external_exports.enum(["backend_exception", "frontend_exception", "request_failure_5xx", "error_log"]),
15724
+ kind: external_exports.enum(["backend_exception", "frontend_exception", "request_failure", "error_log"]),
15674
15725
  description: external_exports.string(),
15675
15726
  event_type: external_exports.enum(["backend_exception", "frontend_exception", "request_event", "log_event"]),
15676
15727
  event_class: external_exports.literal("incident_signal"),
@@ -16091,7 +16142,8 @@ function buildRedactionRecord(bundleBody) {
16091
16142
  function buildVisibilityRecord(input) {
16092
16143
  const routeTarget = input.primarySignal.route_template ?? input.primarySignal.request_path;
16093
16144
  const matchedFields = input.incident.matched_fields.length === 0 ? "none" : input.incident.matched_fields.join(", ");
16094
- const grouping = input.primarySignal.kind === "request_failure_5xx" && input.primarySignal.request_method !== null && routeTarget !== null ? `Repeated 5xx request failures with the same normalized route template, request method, response status, service, and environment reuse this incident fingerprint. This incident currently groups ${input.primarySignal.request_method} ${routeTarget} with matched fields ${matchedFields}.` : `This incident groups repeated failures by fingerprint version ${input.incident.fingerprint_version} inside the service and environment boundary, with matched fields ${matchedFields}.`;
16145
+ const isRequestAnomaly = input.incident.matched_fields.includes("request_anomaly");
16146
+ const grouping = input.primarySignal.kind === "request_failure" && input.primarySignal.request_method !== null && routeTarget !== null ? isRequestAnomaly ? `Repeated request-anomaly incidents with the same normalized route template, request method, response status, service, and environment reuse this incident fingerprint once the anomaly threshold fires. This incident currently groups ${input.primarySignal.request_method} ${routeTarget} with matched fields ${matchedFields}.` : `Repeated request-failure incidents with the same normalized route template, request method, response status, service, and environment reuse this incident fingerprint. This incident currently groups ${input.primarySignal.request_method} ${routeTarget} with matched fields ${matchedFields}.` : `This incident groups repeated failures by fingerprint version ${input.incident.fingerprint_version} inside the service and environment boundary, with matched fields ${matchedFields}.`;
16095
16147
  const spikeLead = input.incident.spike_detected_at === void 0 || input.incident.spike_detected_at === null ? "This incident is not currently marked as spiking." : `This incident was marked as spiking at ${input.incident.spike_detected_at}.`;
16096
16148
  return {
16097
16149
  grouping,
@@ -16102,15 +16154,16 @@ function buildVisibilityRecord(input) {
16102
16154
  }
16103
16155
  function buildSuggestedNextChecks(input) {
16104
16156
  const suggestions = [];
16157
+ const isRequestAnomaly = input.incident.matched_fields.includes("request_anomaly");
16105
16158
  if (input.bundle.status === "pending") {
16106
16159
  suggestions.push("Wait for bundle generation to finish, then rerun the incident context command.");
16107
16160
  } else if (input.bundle.status === "failed") {
16108
16161
  suggestions.push("Inspect bundle generation status or retry bundle retrieval to recover missing context.");
16109
16162
  }
16110
16163
  const routeTarget = input.primarySignal.route_template ?? input.primarySignal.request_path;
16111
- if (input.primarySignal.request_method !== null && input.primarySignal.response_status !== null && input.primarySignal.response_status >= 500) {
16164
+ if (input.primarySignal.kind === "request_failure" && input.primarySignal.request_method !== null && routeTarget !== null) {
16112
16165
  suggestions.push(
16113
- `Inspect the ${input.primarySignal.request_method} ${routeTarget ?? "request"} handler behind this 5xx path.`
16166
+ isRequestAnomaly ? `Inspect the ${input.primarySignal.request_method} ${routeTarget} handler behind this repeated request-anomaly path.` : `Inspect the ${input.primarySignal.request_method} ${routeTarget} handler behind this request-failure path.`
16114
16167
  );
16115
16168
  }
16116
16169
  const firstApplicationFrame = input.primarySignal.first_application_frame;
@@ -16199,12 +16252,13 @@ function deriveIncidentReasonFromSignal(input) {
16199
16252
  };
16200
16253
  case "request_event": {
16201
16254
  const responseStatus = typeof input.response_status === "number" && Number.isFinite(input.response_status) ? input.response_status : null;
16255
+ const isRequestAnomaly = input.request_anomaly === true;
16202
16256
  return {
16203
- kind: "request_failure_5xx",
16204
- description: responseStatus !== null && responseStatus >= 500 ? `request_event response_status=${responseStatus} matched the 5xx request incident rule` : "request_event matched the 5xx request incident rule",
16257
+ kind: "request_failure",
16258
+ description: isRequestAnomaly ? responseStatus !== null ? `request_event response_status=${responseStatus} crossed the repeated request anomaly threshold` : "request_event crossed the repeated request anomaly threshold" : responseStatus !== null ? `request_event response_status=${responseStatus} matched the immediate request failure incident rule` : "request_event matched the immediate request failure incident rule",
16205
16259
  event_type: "request_event",
16206
16260
  event_class: "incident_signal",
16207
- matched_policy: "5xx request failures bypass capture_request_events suppression"
16261
+ matched_policy: isRequestAnomaly ? "Repeated contextual request failures crossed the request anomaly threshold" : "Immediate request failure statuses bypass capture_request_events suppression"
16208
16262
  };
16209
16263
  }
16210
16264
  case "log_event": {
@@ -16511,7 +16565,7 @@ function getRequestResponseStatus(payload) {
16511
16565
  const status = payload?.["response_status"];
16512
16566
  return typeof status === "number" && Number.isFinite(status) ? status : null;
16513
16567
  }
16514
- function classifyEvent(eventType, logLevel, probeActivationId, payload) {
16568
+ function classifyEvent(eventType, logLevel, probeActivationId, payload, capturePreset = "minimal") {
16515
16569
  switch (eventType) {
16516
16570
  case "backend_exception":
16517
16571
  case "frontend_exception":
@@ -16523,10 +16577,7 @@ function classifyEvent(eventType, logLevel, probeActivationId, payload) {
16523
16577
  return "context_signal";
16524
16578
  case "request_event": {
16525
16579
  const responseStatus = getRequestResponseStatus(payload);
16526
- if (responseStatus !== null && responseStatus >= 500) {
16527
- return "incident_signal";
16528
- }
16529
- return "context_signal";
16580
+ return classifyRequestStatus({ responseStatus, capturePreset });
16530
16581
  }
16531
16582
  case "frontend_breadcrumb":
16532
16583
  case "deploy_metadata":
@@ -16596,6 +16647,13 @@ var STORAGE_SCHEMA_MIGRATIONS = [
16596
16647
  ON github_device_authorizations (expires_at)
16597
16648
  `
16598
16649
  ]
16650
+ }),
16651
+ defineStorageSchemaMigration({
16652
+ id: "202605130001_allow_synthetic_webhook_test_deliveries_without_incident_fk",
16653
+ description: "Allow webhook test deliveries to persist without requiring a backing incidents row.",
16654
+ statements: [
16655
+ "ALTER TABLE webhook_deliveries ALTER COLUMN incident_id DROP NOT NULL"
16656
+ ]
16599
16657
  })
16600
16658
  ];
16601
16659
 
@@ -16671,7 +16729,11 @@ function parseLocalIncident(candidate) {
16671
16729
  }
16672
16730
  const serviceRuntime = candidate["service_runtime"];
16673
16731
  const serviceFramework = candidate["service_framework"];
16674
- const incidentReason = deriveIncidentReasonFromSourceEventTypes(candidate["source_event_types"]);
16732
+ const incidentReason = candidate["matched_fields"].includes("request_anomaly") ? deriveIncidentReasonFromSignal({
16733
+ event_type: "request_event",
16734
+ event_class: "incident_signal",
16735
+ request_anomaly: true
16736
+ }) : deriveIncidentReasonFromSourceEventTypes(candidate["source_event_types"]);
16675
16737
  if (serviceRuntime !== null && typeof serviceRuntime !== "string") {
16676
16738
  throw createReadError(400, "invalid_local_state");
16677
16739
  }
@@ -19966,7 +20028,7 @@ function buildPrivacyPreview() {
19966
20028
  sample_event_type: sampleEvent.event_type,
19967
20029
  sample_event_class: sampleEventClass,
19968
20030
  sample_can_create_incident: sampleEventClass === "incident_signal",
19969
- incident_rule: "request_event with response_status >= 500 is classified as an incident_signal",
20031
+ incident_rule: "request_event incident classification follows the resolved capture preset: 5xx always create incidents, balanced also promotes 408/423/424/425/429, and investigative also promotes 409.",
19970
20032
  redacted_fields,
19971
20033
  omitted_fields: [],
19972
20034
  retained_metadata: {
@@ -21668,11 +21730,17 @@ var EVENT_TYPE_SET = new Set(EventTypeValues);
21668
21730
  function isEventType(value) {
21669
21731
  return typeof value === "string" && EVENT_TYPE_SET.has(value);
21670
21732
  }
21671
- function inferSeverity2(eventType) {
21672
- if (eventType === "backend_exception" || eventType === "frontend_exception") {
21733
+ function inferSeverity2(event, capturePreset, incidentKind = "immediate") {
21734
+ if (incidentKind === "request_anomaly") {
21735
+ return "medium";
21736
+ }
21737
+ if (event.event_type === "request_event") {
21738
+ return classifyRequestStatus({ responseStatus: event.payload.response_status, capturePreset }) === "incident_signal" ? "high" : "low";
21739
+ }
21740
+ if (event.event_type === "backend_exception" || event.event_type === "frontend_exception") {
21673
21741
  return "high";
21674
21742
  }
21675
- if (eventType === "error_suppressed") {
21743
+ if (event.event_type === "error_suppressed") {
21676
21744
  return "medium";
21677
21745
  }
21678
21746
  return "low";
@@ -21696,15 +21764,17 @@ function compareEventEnvelopes(left, right) {
21696
21764
  }
21697
21765
  return left.event_id.localeCompare(right.event_id);
21698
21766
  }
21699
- function classifyEnvelope(envelope) {
21767
+ function classifyEnvelope(envelope, capturePreset) {
21700
21768
  return classifyEvent(
21701
21769
  envelope.event_type,
21702
21770
  envelope.event_type === "log_event" ? envelope.payload.level : void 0,
21703
- envelope.event_type === "probe_event" ? envelope.payload.activation_id : void 0
21771
+ envelope.event_type === "probe_event" ? envelope.payload.activation_id : void 0,
21772
+ envelope.payload,
21773
+ capturePreset
21704
21774
  );
21705
21775
  }
21706
- function isIncidentSignalEnvelope(envelope) {
21707
- return classifyEnvelope(envelope) === "incident_signal";
21776
+ function isIncidentSignalEnvelope(envelope, capturePreset) {
21777
+ return classifyEnvelope(envelope, capturePreset) === "incident_signal";
21708
21778
  }
21709
21779
  function getTraceId(envelope) {
21710
21780
  return envelope.correlation?.trace_id ?? null;
@@ -21749,7 +21819,10 @@ function mergeAggregateGroup(aggregates) {
21749
21819
  newEvents: [...canonicalAggregate.newEvents],
21750
21820
  mergedIncidentIds: new Set(canonicalAggregate.mergedIncidentIds),
21751
21821
  signalEventTypes: new Set(canonicalAggregate.signalEventTypes),
21752
- traceIds: new Set(canonicalAggregate.traceIds)
21822
+ traceIds: new Set(canonicalAggregate.traceIds),
21823
+ title: canonicalAggregate.title,
21824
+ kind: canonicalAggregate.kind,
21825
+ severity: canonicalAggregate.severity
21753
21826
  });
21754
21827
  }
21755
21828
  function hashIdentifier(parts, prefix, length) {
@@ -21775,6 +21848,111 @@ function mergeSourceEvents(existingEvents, nextEvents) {
21775
21848
  }
21776
21849
  return [...merged.values()].sort(compareEventEnvelopes);
21777
21850
  }
21851
+ function stableJson2(value) {
21852
+ if (value === null || typeof value !== "object") {
21853
+ return JSON.stringify(value);
21854
+ }
21855
+ if (Array.isArray(value)) {
21856
+ return `[${value.map((entry) => stableJson2(entry)).join(",")}]`;
21857
+ }
21858
+ const record = value;
21859
+ const keys = Object.keys(record).sort();
21860
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson2(record[key])}`).join(",")}}`;
21861
+ }
21862
+ function buildRequestAnomalyFingerprint(input) {
21863
+ return (0, import_node_crypto4.createHash)("sha256").update(
21864
+ stableJson2({
21865
+ kind: "request_status_anomaly",
21866
+ project_id: input.projectId,
21867
+ service_name: input.serviceName,
21868
+ environment: input.environment,
21869
+ method: input.method,
21870
+ route_template: input.routeTemplate,
21871
+ response_status: input.responseStatus
21872
+ })
21873
+ ).digest("hex");
21874
+ }
21875
+ function buildRequestAnomalyTitle(input) {
21876
+ return `Request anomaly: ${input.method} ${input.routeTemplate} returned ${input.responseStatus} repeatedly`;
21877
+ }
21878
+ function toUnixSeconds(occurredAt) {
21879
+ return Math.floor(new Date(occurredAt).getTime() / 1e3);
21880
+ }
21881
+ function countOccurrencesInWindow(events, windowSeconds) {
21882
+ const latestEvent = events.at(-1);
21883
+ if (latestEvent === void 0) {
21884
+ return 0;
21885
+ }
21886
+ const latestOccurredAt = toUnixSeconds(latestEvent.occurred_at);
21887
+ const lowerBound = latestOccurredAt - windowSeconds + 1;
21888
+ return events.filter((event) => {
21889
+ const occurredAt = toUnixSeconds(event.occurred_at);
21890
+ return occurredAt >= lowerBound && occurredAt <= latestOccurredAt;
21891
+ }).length;
21892
+ }
21893
+ function passesRequestAnomalyThreshold(events, threshold) {
21894
+ const occurrences5m = countOccurrencesInWindow(events, 5 * 60);
21895
+ const occurrences1h = countOccurrencesInWindow(events, 60 * 60);
21896
+ const baseline1hPer5m = occurrences1h / 12;
21897
+ const ratio = occurrences5m / Math.max(baseline1hPer5m, 1);
21898
+ return occurrences5m >= threshold.minimum_occurrences_5m && ratio >= threshold.minimum_ratio_5m_to_1h;
21899
+ }
21900
+ function collectRequestAnomalyAggregates(batches, capturePreset) {
21901
+ const grouped = /* @__PURE__ */ new Map();
21902
+ for (const batch of batches) {
21903
+ for (const event of batch.events) {
21904
+ if (event.event_type !== "request_event" || classifyEnvelope(event, capturePreset) !== "context_signal") {
21905
+ continue;
21906
+ }
21907
+ const normalizedEvent = normalizeEvent(event);
21908
+ const responseStatus = normalizedEvent.http_status;
21909
+ const method = normalizedEvent.http_method;
21910
+ const routeTemplate = normalizedEvent.route_template;
21911
+ const threshold = getRequestAnomalyThreshold({ responseStatus, capturePreset });
21912
+ if (threshold === null || responseStatus === null || method === null || routeTemplate === null) {
21913
+ continue;
21914
+ }
21915
+ const projectId = requireProjectId(event);
21916
+ const incidentFingerprint = buildRequestAnomalyFingerprint({
21917
+ projectId,
21918
+ serviceName: event.service.name,
21919
+ environment: event.service.environment,
21920
+ method,
21921
+ routeTemplate,
21922
+ responseStatus
21923
+ });
21924
+ const incidentId = deriveIncidentId(projectId, event.service.name, event.service.environment, incidentFingerprint);
21925
+ const aggregate = grouped.get(incidentId) ?? {
21926
+ incidentId,
21927
+ projectId,
21928
+ serviceName: event.service.name,
21929
+ environment: event.service.environment,
21930
+ fingerprint: incidentFingerprint,
21931
+ matchedFields: /* @__PURE__ */ new Set(["request_anomaly", "route_template", "http_method", "http_status", "environment"]),
21932
+ newEvents: [],
21933
+ mergedIncidentIds: /* @__PURE__ */ new Set([incidentId]),
21934
+ signalEventTypes: /* @__PURE__ */ new Set(["request_event"]),
21935
+ traceIds: /* @__PURE__ */ new Set(),
21936
+ title: buildRequestAnomalyTitle({ method, routeTemplate, responseStatus }),
21937
+ kind: "request_anomaly",
21938
+ severity: "medium"
21939
+ };
21940
+ aggregate.newEvents.push(event);
21941
+ grouped.set(incidentId, aggregate);
21942
+ }
21943
+ }
21944
+ return [...grouped.values()].filter((aggregate) => {
21945
+ const latestEvent = aggregate.newEvents.at(-1);
21946
+ if (latestEvent === void 0 || latestEvent.event_type !== "request_event") {
21947
+ return false;
21948
+ }
21949
+ const threshold = getRequestAnomalyThreshold({
21950
+ responseStatus: normalizeEvent(latestEvent).http_status,
21951
+ capturePreset
21952
+ });
21953
+ return threshold !== null && passesRequestAnomalyThreshold(aggregate.newEvents, threshold);
21954
+ }).sort((left, right) => left.incidentId.localeCompare(right.incidentId));
21955
+ }
21778
21956
  function buildBundleContext(incident) {
21779
21957
  return {
21780
21958
  incident_id: incident.incident_id,
@@ -21804,12 +21982,12 @@ function formatServiceSummary(services) {
21804
21982
  }
21805
21983
  function formatProcessOutput(summary) {
21806
21984
  if (!summary.processed) {
21807
- return summary.message ?? "No new events to process.";
21985
+ return summary.message;
21808
21986
  }
21809
21987
  return [
21810
21988
  `Processed ${summary.events_processed} events from ${summary.files_processed} files into ${summary.incidents_processed} incidents.`,
21811
21989
  ...formatServiceSummary(summary.services),
21812
- `Last processed event file: ${summary.last_processed_event_file ?? "none"}`
21990
+ `Last processed event file: ${summary.last_processed_event_file}`
21813
21991
  ].join("\n");
21814
21992
  }
21815
21993
  async function pathExists4(path, stat) {
@@ -22049,6 +22227,7 @@ async function processCommand(input, dependencies = {}) {
22049
22227
  const statePath = (0, import_node_path11.join)(rootDirectory, LOCAL_STATE_FILE_PATH);
22050
22228
  const bundleDirectoryPath = (0, import_node_path11.join)(rootDirectory, LOCAL_BUNDLE_DIRECTORY_PATH3);
22051
22229
  const reproductionDirectoryPath = (0, import_node_path11.join)(rootDirectory, LOCAL_REPRODUCTION_DIRECTORY_PATH2);
22230
+ const capturePreset = input.preset ?? "minimal";
22052
22231
  await mkdir((0, import_node_path11.join)(rootDirectory, ".debugbundle", "local"), { recursive: true });
22053
22232
  await mkdir(bundleDirectoryPath, { recursive: true });
22054
22233
  await mkdir(reproductionDirectoryPath, { recursive: true });
@@ -22056,22 +22235,26 @@ async function processCommand(input, dependencies = {}) {
22056
22235
  const eventFileNames = await pathExists4(eventsDirectoryPath, stat) ? (await readdir(eventsDirectoryPath)).filter((fileName) => fileName.endsWith(".events.json")).sort() : [];
22057
22236
  const lastProcessedEventFile = previousState?.last_processed_event_file ?? null;
22058
22237
  const newEventFileNames = lastProcessedEventFile === null ? eventFileNames : eventFileNames.filter((fileName) => fileName > lastProcessedEventFile);
22059
- if (newEventFileNames.length === 0) {
22238
+ const processAllEventFiles = input.preset !== void 0;
22239
+ const targetEventFileNames = processAllEventFiles ? eventFileNames : newEventFileNames;
22240
+ if (targetEventFileNames.length === 0) {
22060
22241
  const summary2 = buildNoNewEventsSummary(previousState?.last_processed_event_file ?? eventFileNames.at(-1) ?? null);
22061
22242
  return {
22062
22243
  exitCode: 0,
22063
22244
  output: input.json === true ? JSON.stringify(summary2) : formatProcessOutput(summary2)
22064
22245
  };
22065
22246
  }
22066
- const batches = await readEventBatches(eventsDirectoryPath, newEventFileNames, readFile);
22067
- const incidents = new Map(Object.entries(previousState?.incidents ?? {}));
22247
+ const batches = await readEventBatches(eventsDirectoryPath, targetEventFileNames, readFile);
22248
+ const incidents = new Map(
22249
+ processAllEventFiles ? [] : Object.entries(previousState?.incidents ?? {})
22250
+ );
22068
22251
  const aggregates = /* @__PURE__ */ new Map();
22069
22252
  const traceCorrelationGroups = /* @__PURE__ */ new Map();
22070
22253
  let eventsProcessed = 0;
22071
22254
  for (const batch of batches) {
22072
22255
  for (const event of batch.events) {
22073
22256
  eventsProcessed += 1;
22074
- if (!isIncidentSignalEnvelope(event)) {
22257
+ if (!isIncidentSignalEnvelope(event, capturePreset)) {
22075
22258
  continue;
22076
22259
  }
22077
22260
  const normalizedEvent = normalizeEvent(event);
@@ -22088,7 +22271,10 @@ async function processCommand(input, dependencies = {}) {
22088
22271
  newEvents: [],
22089
22272
  mergedIncidentIds: /* @__PURE__ */ new Set([incidentId]),
22090
22273
  signalEventTypes: /* @__PURE__ */ new Set(),
22091
- traceIds: /* @__PURE__ */ new Set()
22274
+ traceIds: /* @__PURE__ */ new Set(),
22275
+ title: normalizedEvent.normalized_message,
22276
+ kind: "immediate",
22277
+ severity: inferSeverity2(event, capturePreset)
22092
22278
  };
22093
22279
  for (const matchedField of inferMatchedFields(normalizedEvent)) {
22094
22280
  aggregate.matchedFields.add(matchedField);
@@ -22155,14 +22341,16 @@ async function processCommand(input, dependencies = {}) {
22155
22341
  mergedAggregatesByRoot.set(rootIncidentId, aggregateGroup);
22156
22342
  }
22157
22343
  const mergedAggregates = [...mergedAggregatesByRoot.values()].map((aggregateGroup) => mergeAggregateGroup(aggregateGroup)).sort((left, right) => left.incidentId.localeCompare(right.incidentId));
22344
+ const requestAnomalyAggregates = input.preset === void 0 ? [] : collectRequestAnomalyAggregates(batches, capturePreset);
22345
+ const finalizedAggregates = [...mergedAggregates, ...requestAnomalyAggregates].sort((left, right) => left.incidentId.localeCompare(right.incidentId));
22158
22346
  const services = /* @__PURE__ */ new Map();
22159
- for (const aggregate of mergedAggregates) {
22347
+ for (const aggregate of finalizedAggregates) {
22160
22348
  const incidentId = aggregate.incidentId;
22161
22349
  const existingIncidents = [...aggregate.mergedIncidentIds].map((mergedIncidentId) => incidents.get(mergedIncidentId)).filter((incident2) => incident2 !== void 0);
22162
22350
  const existing = existingIncidents.find((incident2) => incident2.incident_id === incidentId) ?? existingIncidents[0];
22163
22351
  const existingSourceEvents = existingIncidents.flatMap((incident2) => incident2.source_events);
22164
22352
  const combinedSourceEvents = mergeSourceEvents(existingSourceEvents, aggregate.newEvents);
22165
- const signalEvents = combinedSourceEvents.filter(isIncidentSignalEnvelope);
22353
+ const signalEvents = aggregate.kind === "request_anomaly" ? combinedSourceEvents : combinedSourceEvents.filter((event) => isIncidentSignalEnvelope(event, capturePreset));
22166
22354
  if (signalEvents.length === 0) {
22167
22355
  continue;
22168
22356
  }
@@ -22172,8 +22360,7 @@ async function processCommand(input, dependencies = {}) {
22172
22360
  continue;
22173
22361
  }
22174
22362
  const sourceEventTypes = [...new Set(signalEvents.map((event) => event.event_type))].sort();
22175
- const severity = signalEvents.map((event) => inferSeverity2(event.event_type)).sort((left, right) => severityRank(right) - severityRank(left))[0] ?? "low";
22176
- const latestNormalizedEvent = normalizeEvent(latestSignalEvent);
22363
+ const severity = signalEvents.map((event) => inferSeverity2(event, capturePreset, aggregate.kind)).sort((left, right) => severityRank(right) - severityRank(left))[0] ?? aggregate.severity;
22177
22364
  const generationNumber = signalEvents.length;
22178
22365
  const bundlePath = `${LOCAL_BUNDLE_DIRECTORY_PATH3}/${incidentId}.bundle.json`;
22179
22366
  const reproductionPath = `${LOCAL_REPRODUCTION_DIRECTORY_PATH2}/${incidentId}.reproduction.json`;
@@ -22188,7 +22375,7 @@ async function processCommand(input, dependencies = {}) {
22188
22375
  environment: aggregate.environment,
22189
22376
  fingerprint: aggregate.fingerprint,
22190
22377
  fingerprint_version: FINGERPRINT_VERSION,
22191
- title: latestNormalizedEvent.normalized_message,
22378
+ title: aggregate.title,
22192
22379
  severity,
22193
22380
  status: existingIncidents.some((incidentState) => incidentState.status === "resolved") ? "open" : existing?.status ?? "open",
22194
22381
  first_seen_at: firstSignalEvent.occurred_at,
@@ -22249,21 +22436,22 @@ async function processCommand(input, dependencies = {}) {
22249
22436
  incidents.set(incidentId, incident);
22250
22437
  services.set(incident.service_name, (services.get(incident.service_name) ?? 0) + 1);
22251
22438
  }
22439
+ const finalProcessedEventFile = targetEventFileNames[targetEventFileNames.length - 1];
22252
22440
  const nextState = {
22253
22441
  version: 1,
22254
- last_processed_event_file: newEventFileNames.at(-1) ?? previousState?.last_processed_event_file ?? null,
22442
+ last_processed_event_file: finalProcessedEventFile,
22255
22443
  incidents: Object.fromEntries([...incidents.entries()].sort(([left], [right]) => left.localeCompare(right)))
22256
22444
  };
22257
22445
  await writeFile(statePath, serializeState(nextState));
22258
22446
  const summary = buildProcessedSummary({
22259
22447
  filesProcessed: newEventFileNames.length,
22260
22448
  eventsProcessed,
22261
- incidentsProcessed: mergedAggregates.length,
22449
+ incidentsProcessed: finalizedAggregates.length,
22262
22450
  services: [...services.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([service, count]) => ({
22263
22451
  service,
22264
22452
  incidents: count
22265
22453
  })),
22266
- lastProcessedEventFile: nextState.last_processed_event_file
22454
+ lastProcessedEventFile: finalProcessedEventFile
22267
22455
  });
22268
22456
  return {
22269
22457
  exitCode: 0,
@@ -24248,14 +24436,14 @@ function localFailureStepName(checks) {
24248
24436
  function cloudVerificationRunId(now) {
24249
24437
  return now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
24250
24438
  }
24251
- function requestFailure5xxReason() {
24439
+ function requestFailureReason() {
24252
24440
  const incidentReason = deriveIncidentReasonFromSignal({
24253
24441
  event_type: "request_event",
24254
24442
  event_class: "incident_signal",
24255
24443
  response_status: 503
24256
24444
  });
24257
24445
  if (incidentReason === null) {
24258
- throw new Error("request_failure_5xx_reason_unavailable");
24446
+ throw new Error("request_failure_reason_unavailable");
24259
24447
  }
24260
24448
  return incidentReason;
24261
24449
  }
@@ -24530,7 +24718,7 @@ async function verifyCloudCommand(input, dependencies = {}) {
24530
24718
  const verification = {
24531
24719
  mode: "active_5xx",
24532
24720
  bundle_status: "unknown",
24533
- classification_reason: requestFailure5xxReason()
24721
+ classification_reason: requestFailureReason()
24534
24722
  };
24535
24723
  const errors = [];
24536
24724
  let exitCode = 0;
@@ -24582,7 +24770,7 @@ async function verifyCloudCommand(input, dependencies = {}) {
24582
24770
  if (candidate !== void 0) {
24583
24771
  incidentId = candidate.incident_id;
24584
24772
  verification.incident_id = candidate.incident_id;
24585
- verification.classification_reason = candidate.incident_reason ?? requestFailure5xxReason();
24773
+ verification.classification_reason = candidate.incident_reason ?? requestFailureReason();
24586
24774
  break;
24587
24775
  }
24588
24776
  if (attempt < pollAttempts) {
@@ -24857,7 +25045,7 @@ var CLI_USAGE_LINES = [
24857
25045
  " debugbundle connect [--auth-file <path>] [--json]",
24858
25046
  " debugbundle ingest <file> --format <debugbundle-ndjson|php-error|apache-error> [--json]",
24859
25047
  " debugbundle watch [--cloud] --log <file> --format <debugbundle-ndjson|php-error|apache-error> [--json]",
24860
- " debugbundle process [--json]",
25048
+ " debugbundle process [--preset <minimal|balanced|investigative>] [--json]",
24861
25049
  " debugbundle clean [--events] [--bundles] [--all] [--older-than <Nd>] [--json]",
24862
25050
  " debugbundle validate [--fix] [--json]",
24863
25051
  " debugbundle verify local [--json]",
@@ -28761,10 +28949,17 @@ ${formatUsage()}`
28761
28949
  });
28762
28950
  }
28763
28951
  if (command === "process") {
28764
- expectNoUnknownOptions(parsedArgv, ["json"]);
28952
+ expectNoUnknownOptions(parsedArgv, ["json", "preset"]);
28765
28953
  ensureNoExtraPositionals(parsedArgv, 1);
28766
28954
  const json = readBooleanOption(parsedArgv, "json");
28767
- return await (dependencies.processCommand ?? processCommand)(json === true ? { json: true } : {});
28955
+ const preset = readStringOption(parsedArgv, "preset");
28956
+ if (preset !== void 0 && !CapturePresetSchema.safeParse(preset).success) {
28957
+ throw new CliInputError("Invalid value for --preset.");
28958
+ }
28959
+ return await (dependencies.processCommand ?? processCommand)({
28960
+ ...json === true ? { json: true } : {},
28961
+ ...preset !== void 0 ? { preset } : {}
28962
+ });
28768
28963
  }
28769
28964
  if (command === "clean") {
28770
28965
  expectNoUnknownOptions(parsedArgv, ["events", "bundles", "all", "older-than", "json"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugbundle/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Command-line interface for DebugBundle",
6
6
  "license": "AGPL-3.0-only",