Skip to content

feat(flow-webhook-triage): add AppSignal error triage plugin - #8

Open
AmishaBisht wants to merge 5 commits into
mainfrom
feat/flow-webhook-triage
Open

feat(flow-webhook-triage): add AppSignal error triage plugin#8
AmishaBisht wants to merge 5 commits into
mainfrom
feat/flow-webhook-triage

Conversation

@AmishaBisht

@AmishaBisht AmishaBisht commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Adds a flow-webhook-triage plugin: pulls incidents from the flow_webhooks and
flow_webhook_config_errors namespaces, diagnoses each against the glific/glific
code, and upserts a row into a shared Google Sheet.

Why a sheet

A single incident is noise. A month of diagnosed incidents answers two questions
AppSignal can't:

  • What's accumulating in the :unknown bucket? :unknown is the fail-safe, so
    anything a webhook can't name pages on-call unclassified. The pattern only shows up
    over weeks.
  • Which config errors repeat? One NGO making a mistake is a support conversation.
    Many NGOs making the same mistake is a product defect — validate it at the source
    instead of triaging it forever.

Each incident gets one row that persists, with a times_seen counter that increments
every run it reappears, so repeat offenders sort straight to the top.

Design notes

Dedup is on incident_id, server-side, under a lock. A recurrence updates the
existing row and bumps times_seen rather than appending — so re-runs are idempotent,
two people triaging the same day can't double-write, and the diagnosis isn't
re-litigated on every run.

No Google credentials are shared. Sheet writes go through an Apps Script Web App
that the sheet owner deploys as themselves. This is load-bearing: the Sheets API
requires OAuth for any write regardless of link-sharing, so "share the sheet and
hardcode the id" cannot work — an API key only ever grants read access to published
sheets. The deployment URL is the only secret and lives in an env var.

The taxonomy is read from the code, not inferrederrors.ex, error_type.ex,
error_reporter.ex, instrumentation.ex. Worth knowing:
rate_limited/service_unavailable are :system, not suppressed, because there is no
retry — an upstream blip is a real page. references/error-taxonomy.md cites files and
asks the reader to re-verify, since this subsystem has moved fast (#5351, #5357, #5388).

AppSignal needs a Personal API token, not the push key the app uses to send errors.
Push keys are write-only and 401 on read. The script says so explicitly on a 401 — this
is a very easy mistake to make and the error is otherwise opaque.

Verified vs not

✅ Sheet upsert / dedup / auth / watermark 17 checks via test-upsert.js — stubs SpreadsheetApp so Code.gs can run in Node
✅ AppSignal 401 path exercised against a real push key; prints the fix, not a stack trace
The GraphQL query written from AppSignal's docs, never run against a readable token

That last row is the main review risk. Field names may be wrong; fetch-incidents.mjs introspect dumps the live schema to correct QUERY on first run. The output contract
(one object per incident, with tags) is what the rest of the skill depends on. The
limitation is flagged in the script and in sheet-setup.md rather than left to look
tested.

test-upsert.js already earned its keep — it caught an in-batch duplicate incident_id
taking the update path and writing to row -1, which throws in real Apps Script and
fails the whole batch.

Overlap worth resolving

There's an unmerged glific-monitoring skill (on the system-monitoring-skill branch,
also present untracked in some local marketplace checkouts) doing AppSignal → classify →
Discord → autofix PRs. It overlaps this on "pull AppSignal, classify" but differs on
everything else: prod-wide vs these two namespaces, Discord vs sheet, no incremental
resume, no month-scale analysis. Kept separate deliberately rather than entangling
with an unmerged branch — but whoever owns it should weigh in before both land.

Setup (both one-time, in references/sheet-setup.md)

  1. A Personal API token in APPSIGNAL_API_KEY — verify with
    node scripts/fetch-incidents.mjs verify
  2. Deploy the bundled Apps Script against a sheet; set GLIFIC_TRIAGE_SHEET_URL +
    GLIFIC_TRIAGE_TOKEN

Both scripts dry-run without their env vars, so you can see the shape before creating
anything.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a flow-webhook triage plugin to help review AppSignal incidents across system and configuration error namespaces.
    • Introduced automated diagnosis with recurring detection and watermark-based progress tracking.
    • Integrated with a shared Google Sheet to deduplicate results and increment recurrence counts.
    • Added CLI tools to fetch incidents and append/redact results securely.
  • Documentation

    • Added plugin and skill documentation, including setup, workflow details, and a referenced error taxonomy.

Adds a plugin that triages Glific flow-webhook errors: pulls incidents from
the flow_webhooks and flow_webhook_config_errors namespaces, diagnoses each
against the glific/glific code, and upserts a row into a shared Google Sheet.

The sheet is the point. A single incident is noise; a month of diagnosed ones
shows which failures are worth engineering away — what is accumulating in the
:unknown bucket, and which config errors repeat across enough orgs to be a
product defect rather than a support ticket.

Notes on the design:

- Dedup key is incident_id, enforced server-side under a lock. Recurrence
  updates the row and bumps times_seen rather than appending, so repeat
  offenders sort to the top and concurrent runs cannot double-write.

- Sheet writes go through an Apps Script Web App deployed by the sheet owner
  ("execute as: me"), so no Google credentials are shared. Sheets API writes
  require OAuth regardless of link-sharing, so a hardcoded sheet id alone
  cannot work.

- Incident data comes from the AppSignal GraphQL API, which needs a Personal
  API token. The push key the app uses to send errors is write-only and 401s.

- The classification rules in references/error-taxonomy.md are read from the
  code (errors.ex, error_type.ex, error_reporter.ex, instrumentation.ex), not
  inferred. Notably rate_limited/service_unavailable are :system, not
  suppressed, because there is no retry.

Code.gs cannot run outside Google, so test-upsert.js stubs SpreadsheetApp and
exercises it in Node (17 checks). It already caught a real bug: an in-batch
duplicate id took the update path and wrote to row -1, failing the batch.

The GraphQL query itself is unverified against a live token — an introspect
command is included to correct it on first run, and the limitation is flagged
in the script and the setup doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds and registers the flow-webhook-triage Claude plugin. The plugin documents daily AppSignal incident triage, code-grounded diagnosis, taxonomy rules, setup, and pattern analysis. New Node.js scripts fetch incidents through AppSignal GraphQL, redact submitted fields, and communicate with a Google Apps Script web app. The Apps Script initializes a triage sheet, exposes watermark and upsert endpoints, authenticates requests, tracks recurring incidents, and serializes writes with a lock. Node.js test harnesses cover redaction and sheet behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding the AppSignal flow-webhook triage plugin.
Description check ✅ Passed The description matches the implemented triage workflow, sheet upserts, redaction, and setup/testing changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`:
- Around line 169-179: Update the recurrence patch construction in the
existing-row flow so last_seen, occurrences, and http_status are included only
when present in the incoming payload, preserving their existing sheet values
when omitted. Use nullish checks rather than truthiness checks so valid zero
values are retained, while continuing to update the other recurrence fields.
- Around line 169-179: Sanitize every untrusted string value before it reaches
either the patch loop’s setValue call or any setValues call in the surrounding
write flow, prefixing values that could be interpreted as formulas while
preserving non-string values and existing data semantics. Update the shared
write/sanitization path rather than only the visible patch object, and use it
consistently for all AppSignal fields.
- Around line 47-50: Make expectedToken_ treat a missing TRIAGE_TOKEN as a
configuration error instead of returning an open state, and update the request
validation around Code.gs lines 127-130 to reject requests unless the configured
token matches. In
plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md
lines 34-36, remove the documented fail-open/test exception.
- Around line 160-180: Update the batch-processing logic around pendingIds and
the index branch to track every processed incident ID, including IDs found in
index, before performing updates. Skip subsequent occurrences of any ID within
the same batch so an existing incident is updated and times_seen incremented
only once. Add a regression test covering duplicate persisted IDs in one batch.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`:
- Around line 42-60: Update the response handling in fetchIncidents so the
existing 401/authentication-specific error check runs first, then reject every
response where res.ok is false, including JSON 403, 429, and 5xx responses
without body.errors. Preserve the existing GraphQL error handling and successful
body.data return path.
- Around line 36-40: Bound both external requests with a finite abort timeout:
update fetch-incidents.mjs around the fetch call to pass an AbortController
signal, and apply the same timeout to the fetch call in append-to-sheet.mjs.
Ensure append-to-sheet.mjs reports timeout failures clearly while preserving
existing error handling for other failures.
- Around line 118-123: Update the output object constructed in the incidents
loop of fetch-incidents to map each returned incident’s id to the
downstream-required incident_id field. Preserve the namespace fallback and all
other incident properties while ensuring every emitted row includes incident_id.
- Around line 118-121: The incident retrieval loop around gql in
fetch-incidents.mjs must not treat a 100-item response as complete: add
pagination using the API’s supported cursor/offset fields and continue fetching
until a page is shorter than the limit, while preserving the existing namespace
mapping and output behavior.

In `@plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md`:
- Around line 65-67: Update the incident capture guidance in the
triage-flow-webhooks skill to explicitly map AppSignal fields to the downstream
sheet columns: incident number to incident_id, count to occurrences,
organization_id to org_id, and include appsignal_url. Preserve the existing
exception, timing, and tag fields while naming the exact JSON keys expected by
Code.gs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 675c1e47-93eb-4904-a841-982c4dd73d77

📥 Commits

Reviewing files that changed from the base of the PR and between 793351b and 0cfc3bd.

📒 Files selected for processing (10)
  • .claude-plugin/marketplace.json
  • plugins/flow-webhook-triage/.claude-plugin/plugin.json
  • plugins/flow-webhook-triage/README.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/error-taxonomy.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/append-to-sheet.mjs
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/test-upsert.js
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs

Comment on lines +47 to +50
/** Shared secret, set in Script Properties. Absent = open, which is fine for a private URL. */
function expectedToken_() {
return PropertiesService.getScriptProperties().getProperty('TRIAGE_TOKEN');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when TRIAGE_TOKEN is not configured.

The public Web App currently grants unauthenticated write access whenever the property is accidentally omitted.

  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L47-L50: treat a missing token as a configuration error.
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L127-L130: reject requests unless a configured token matches.
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md#L34-L36: remove the documented fail-open/test exception.
📍 Affects 2 files
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L47-L50 (this comment)
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L127-L130
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md#L34-L36
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 47 - 50, Make expectedToken_ treat a missing TRIAGE_TOKEN as a
configuration error instead of returning an open state, and update the request
validation around Code.gs lines 127-130 to reject requests unless the configured
token matches. In
plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md
lines 34-36, remove the documented fail-open/test exception.

Comment on lines +169 to +179
const patch = {
last_seen: row.last_seen || '',
last_run_date: today,
times_seen: prev + 1,
occurrences: row.occurrences || '',
http_status: row.http_status || '',
};
for (const key of Object.keys(patch)) {
const col = COLUMNS.indexOf(key);
if (col >= 0) sh.getRange(existing, col + 1).setValue(patch[key]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve existing values when recurrence fields are omitted.

The client only requires incident_id, but omitted last_seen, occurrences, or http_status are written as empty strings. This can erase the watermark and other previously captured data. Only patch fields present in the payload, using nullish checks so valid zero values survive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 169 - 179, Update the recurrence patch construction in the
existing-row flow so last_seen, occurrences, and http_status are included only
when present in the incoming payload, preserving their existing sheet values
when omitted. Use nullish checks rather than truthiness checks so valid zero
values are retained, while continuing to update the other recurrence fields.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs'

echo '### Relevant excerpt (around lines 130-230)'
sed -n '130,230p' "$FILE" | cat -n

echo
echo '### All setValue/setValues occurrences in Code.gs'
rg -n 'setValue|setValues' "$FILE"

Repository: glific/claude-plugins

Length of output: 3414


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs'

echo '### Sanitization helpers / formula handling in Code.gs'
rg -n "sanitize|formula|escape|apostrophe|='|^\s*const .*safe|replace\(" "$FILE" || true

echo
echo '### Full file length'
wc -l "$FILE"

echo
echo '### File header / helper definitions'
sed -n '1,140p' "$FILE" | cat -n

Repository: glific/claude-plugins

Length of output: 5722


Escape untrusted Sheet values before writing them.
AppSignal fields are written raw via both setValue() and setValues(), so any string starting with = can become a formula in Google Sheets. Prefix/sanitize all string cell values before both write paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 169 - 179, Sanitize every untrusted string value before it reaches
either the patch loop’s setValue call or any setValues call in the surrounding
write flow, prefixing values that could be interpreted as formulas while
preserving non-string values and existing data semantics. Update the shared
write/sanitization path rather than only the visible patch object, and use it
consistently for all AppSignal fields.

Comment on lines +36 to +40
const res = await fetch(`${ENDPOINT}?token=${encodeURIComponent(TOKEN)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound both external requests with timeouts.

A stalled AppSignal or Apps Script connection can hang the CLI indefinitely.

  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs#L36-L40: attach an abort signal with a finite timeout.
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/append-to-sheet.mjs#L38-L40: apply the same timeout and report timeout failures clearly.
📍 Affects 2 files
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs#L36-L40 (this comment)
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/append-to-sheet.mjs#L38-L40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 36 - 40, Bound both external requests with a finite abort timeout:
update fetch-incidents.mjs around the fetch call to pass an AbortController
signal, and apply the same timeout to the fetch call in append-to-sheet.mjs.
Ensure append-to-sheet.mjs reports timeout failures clearly while preserving
existing error handling for other failures.

Comment on lines +42 to +60
const text = await res.text();
let body;
try {
body = JSON.parse(text);
} catch {
die(`non-JSON response (HTTP ${res.status}): ${text.slice(0, 300)}`);
}

if (res.status === 401 || body.errors?.some((e) => /authenticat/i.test(e.message || ''))) {
die(
'AppSignal rejected the token (401).\n' +
' This is almost always a PUSH key rather than a Personal API token.\n' +
' Push keys live on the app settings page and can only SEND data.\n' +
' Get a Personal API token from your AppSignal *user* settings, and put it\n' +
' in APPSIGNAL_API_KEY.'
);
}
if (body.errors) die(`GraphQL: ${JSON.stringify(body.errors).slice(0, 500)}`);
return body.data;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject every unsuccessful HTTP response.

A JSON 403, 429, or 5xx response without body.errors returns undefined; fetchIncidents() then silently reports zero incidents. Handle the 401 message first, then fail on !res.ok.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 42 - 60, Update the response handling in fetchIncidents so the
existing 401/authentication-specific error check runs first, then reject every
response where res.ok is false, including JSON 403, 429, and 5xx responses
without body.errors. Preserve the existing GraphQL error handling and successful
body.data return path.

Comment on lines +118 to +121
const data = await gql(QUERY, { appId: APP_ID, namespace, limit: 100, start: since });
const incidents = data?.app?.exceptionIncidents || [];
for (const i of incidents) out.push({ ...i, namespace: i.namespace || namespace });
console.error(`${namespace}: ${incidents.length} incident(s)`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the reported lines
sed -n '1,220p' plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs

printf '\n--- SEARCH exceptionIncidents ---\n'
rg -n "exceptionIncidents|pageInfo|cursor|pagination|limit: 100|start: since|QUERY" plugins/flow-webhook-triage -S

printf '\n--- FILE LISTING IN SCRIPT DIR ---\n'
git ls-files plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts

Repository: glific/claude-plugins

Length of output: 6358


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '100,150p' plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md

printf '\n--- SEARCH TRIAGE DOCS ---\n'
rg -n "fetch-incidents|incident(s)?|limit: 100|pagination|watermark|since" plugins/flow-webhook-triage/skills/triage-flow-webhooks -S

Repository: glific/claude-plugins

Length of output: 18120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,90p' plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md

printf '\n--- WATERMARK / FETCH REFERENCES ---\n'
sed -n '90,150p' plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md

Repository: glific/claude-plugins

Length of output: 7213


Avoid truncating incident fetches at 100. plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs:118-121 always requests limit: 100 and treats the page as complete. If AppSignal returns a full page, later incidents in the watermark window can be skipped permanently. Paginate, or fail when a page hits 100 until pagination is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 118 - 121, The incident retrieval loop around gql in
fetch-incidents.mjs must not treat a 100-item response as complete: add
pagination using the API’s supported cursor/offset fields and continue fetching
until a page is shorter than the limit, while preserving the existing namespace
mapping and output behavior.

Comment on lines +118 to +123
const data = await gql(QUERY, { appId: APP_ID, namespace, limit: 100, start: since });
const incidents = data?.app?.exceptionIncidents || [];
for (const i of incidents) out.push({ ...i, namespace: i.namespace || namespace });
console.error(`${namespace}: ${incidents.length} incident(s)`);
}
console.log(JSON.stringify(out, null, 2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Emit the downstream deduplication key as incident_id.

The query returns id, while append-to-sheet.mjs rejects every row without incident_id. Normalize it when constructing the output.

Proposed fix
-    for (const i of incidents) out.push({ ...i, namespace: i.namespace || namespace });
+    for (const i of incidents) {
+      out.push({
+        ...i,
+        incident_id: String(i.id),
+        namespace: i.namespace || namespace,
+      });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = await gql(QUERY, { appId: APP_ID, namespace, limit: 100, start: since });
const incidents = data?.app?.exceptionIncidents || [];
for (const i of incidents) out.push({ ...i, namespace: i.namespace || namespace });
console.error(`${namespace}: ${incidents.length} incident(s)`);
}
console.log(JSON.stringify(out, null, 2));
const data = await gql(QUERY, { appId: APP_ID, namespace, limit: 100, start: since });
const incidents = data?.app?.exceptionIncidents || [];
for (const i of incidents) {
out.push({
...i,
incident_id: String(i.id),
namespace: i.namespace || namespace,
});
}
console.error(`${namespace}: ${incidents.length} incident(s)`);
}
console.log(JSON.stringify(out, null, 2));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 118 - 123, Update the output object constructed in the incidents
loop of fetch-incidents to map each returned incident’s id to the
downstream-required incident_id field. Preserve the namespace fallback and all
other incident properties while ensuring every emitted row includes incident_id.

Comment on lines +65 to +67
Per incident, capture: number, exception class, count, first/last seen, and the tags —
`webhook_name`, `error_type`, `kaapi_error_type`, `http_status`, `organization_id`, `flow_id`,
`contact_id`, `reason`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Map AppSignal fields to expected sheet columns explicitly.

The downstream Code.gs script relies on specific column names (incident_id, occurrences, org_id, appsignal_url). To prevent the LLM from dropping data or hallucinating keys when building the JSON payload, explicitly map the captured data fields to the expected column names and include the missing appsignal_url.

💡 Proposed fix
-Per incident, capture: number, exception class, count, first/last seen, and the tags —
-`webhook_name`, `error_type`, `kaapi_error_type`, `http_status`, `organization_id`, `flow_id`,
-`contact_id`, `reason`.
+Per incident, capture: incident_id (number), exception class, occurrences (count), first/last seen, appsignal_url, and the tags —
+`webhook_name`, `error_type`, `kaapi_error_type`, `http_status`, `org_id` (from organization_id), `flow_id`,
+`contact_id`, `reason`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Per incident, capture: number, exception class, count, first/last seen, and the tags —
`webhook_name`, `error_type`, `kaapi_error_type`, `http_status`, `organization_id`, `flow_id`,
`contact_id`, `reason`.
Per incident, capture: incident_id (number), exception class, occurrences (count), first/last seen, appsignal_url, and the tags —
`webhook_name`, `error_type`, `kaapi_error_type`, `http_status`, `org_id` (from organization_id), `flow_id`,
`contact_id`, `reason`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md` around
lines 65 - 67, Update the incident capture guidance in the triage-flow-webhooks
skill to explicitly map AppSignal fields to the downstream sheet columns:
incident number to incident_id, count to occurrences, organization_id to org_id,
and include appsignal_url. Preserve the existing exception, timing, and tag
fields while naming the exact JSON keys expected by Code.gs.

AmishaBisht and others added 4 commits July 17, 2026 13:46
The deploy dialog offers "Anyone", not "Anyone with the link" — the latter is
Google's older label and does not appear in the current UI. Found while
following the setup for real.

Also spells out why the other three options fail: they all require the *caller*
to be signed into Google, and the script POSTs with a plain fetch that has no
session, so it gets a login page (HTML) instead of JSON. The append client's
error for that case now says so directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e sheet

Raised in review: the sheet is written by a Web App deployed with "Who has
access: Anyone", so what ends up in it deserves more care than I gave it.

Two fixes.

**doGet was unauthenticated.** doPost checked TRIAGE_TOKEN; doGet did not. Since
"Anyone" is what lets an unauthenticated client call the endpoint at all, that
left the watermark — and every incident id in the sheet — readable by anyone who
obtained the URL. Reads now require the token, with a regression guard so it
cannot be dropped silently.

**Free text is now redacted at the boundary.** Glific carries WhatsApp traffic,
so a provider error string can quote a contact's phone number or echo a
credential, and the sheet outlives AppSignal's access controls. Phone numbers,
emails, credentials, JWTs, UUIDs and opaque blobs are scrubbed in
append-to-sheet.mjs before send.

Writing the tests immediately found a real leak: "Authorization: Bearer sk-xxx"
matched the label, consumed "Bearer" as the value, and left the actual secret in
the string — output that looked redacted but was not. Fixed with an explicit
`(?:bearer\s+)?` step, and the test now covers it.

Redaction is a backstop, not a guarantee, and the docs say so plainly. A regex
cannot catch a name in prose or a sentence a contact typed; test-redact.mjs
keeps those cases visible under "KNOWN LIMITS" rather than implying coverage the
code does not have. The controls that actually hold are documented instead: keep
sheet sharing tight, paraphrase rather than quote, and link the incident when the
raw text is genuinely needed. contact_id is deliberately not a column.

Also documents that the token and the sheet are separate doors — the token grants
the endpoint, not the data. That distinction is easy to conflate and worth
stating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 401 handler asserted "this is almost always a PUSH key rather than a
Personal API token". That was a hunch hard-coded as a diagnosis, and it cost
real time: it sent the user to the wrong AppSignal page repeatedly while the
actual failure was elsewhere, and I kept quoting the message back as if it were
evidence.

It now prints AppSignal's own error and lists candidate causes in order — wrong
token type, no access to the app, truncated paste, regenerated token — without
claiming to know which. An error string that names a cause gets trusted like a
measurement; a wrong one manufactures confident false leads.

Docs confirm the auth method itself is correct: personal API token as a query
parameter against https://appsignal.com/graphql. Also adds the exact URL for
the token (https://appsignal.com/users/edit) rather than "your user settings",
which was not findable enough to act on.

Renames GLIFIC_TRIAGE_SHEET_URL -> GLIFIC_TRIAGE_WEBAPP_URL. The old name reads
as "the URL of the sheet", and predictably got set to the docs.google.com URL
instead of the Apps Script /exec deployment URL. The value is the deployment,
not the document.

Documents the one-shared-sheet model, which the setup docs had backwards: the
team writes to a single sheet via the owner's deployment, so most people set
three env vars and never touch Apps Script. Per-person sheets would defeat the
dedup and the trend analysis that are the entire point.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Running this against prod for the first time invalidated the core assumption.

**One AppSignal incident is not one failure.** AppSignal groups by exception
class, and this subsystem deliberately uses low-cardinality classes (the webhook
name lives in the message, not the class — which is why the AppSignal trigger had
to exist at all). So incident #511 "SystemError", 8958 occurrences, held samples
from text_to_speech, filesearch-gpt, voice-filesearch-gpt AND speech_to_text.
A row per incident would have been precise-looking and useless.

The unit of triage is now a SIGNATURE mined from samples:
(namespace, exception_class, webhook_name, error_type, reason_shape), hashed to
signature_key. reason_shape normalises ids out ("6367551 does not have any active
flows" -> "<N> does not have any active flows") so the same failure groups across
contacts. The incident number is demoted to a link. Today's 3 incidents resolve
to 8 distinct, diagnosable signatures.

Also adds org_count — distinct orgs per signature. That is the product-defect
signal the whole sheet exists for: one org repeating is a support conversation,
many orgs on one config error is something to fix at source.

**Query corrected against the live schema** (it had never been run):
`namespaces` is plural and takes [String]; exceptionIncidents has no time filter
but samples(start:, end:) does; tags are key/value pairs under sample.overview,
not fields. Counts are sample counts, not true volume — named sample_count, with
incident_count kept alongside for context, and the sample cap logged rather than
silently truncating.

**Header is no longer write-once.** It was written only when the sheet was empty,
so changing COLUMNS left a 24-column header above 28-column data: `occurrences`
sat above sample_count, `org_id` above incident_count. Correct data, every label
wrong, no error anywhere — it rendered perfectly. sheet_() now reconciles the
header on every write, with a regression test reproducing the stale-header case.

Verified end to end against the live deployment: 8 rows inserted, re-run gives
updated 8 / total 8, tokenless read rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md (1)

29-29: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update deduplication key terminology to match the signature-based grouping.

The PR changed the deduplication strategy from incident_id to normalized failure signatures (signature_key), and append-to-sheet.mjs now enforces signature_key as the required deduplication key. However, this documentation still references incident_id and incidentIds.

💡 Proposed fixes for stale terminology

Update line 29:

-Sheet: dedup key is incident_id; recurrence bumps times_seen
+Sheet: dedup key is signature_key; recurrence bumps times_seen

You should also update the references across the rest of the file:

  • Line 43: Returns { lastSeen, incidentIds, rowCount }.signatureKeys
  • Line 46: - incidentIds — already in the sheet.signatureKeys
  • Line 85: Only for incidents not in incidentIds.signatureKeys
  • Line 159: Recurring incidents need only the volatile fields (incident_id, ...signature_key
  • Line 168: The script dedups server-side on incident_id under a locksignature_key
  • Line 216: If incidentIds has it,signatureKeys
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md` at line 29,
Update the triage-flow-webhooks documentation to consistently use
signature-based deduplication terminology: replace incident_id and incidentIds
references with signature_key and signatureKeys in the deduplication
description, return documentation, filtering guidance, recurring-incident
payload guidance, server-side deduplication note, and final lookup instruction.
♻️ Duplicate comments (4)
plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs (2)

39-43: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound external requests with a timeout.

A stalled connection to AppSignal can cause the CLI to hang indefinitely because Node's fetch does not have a default timeout. Please apply an AbortController with a finite timeout to this request (and similarly in append-to-sheet.mjs).

💡 Proposed fix
+  const controller = new AbortController();
+  const id = setTimeout(() => controller.abort(), 15000);
   const res = await fetch(`${ENDPOINT}?token=${encodeURIComponent(TOKEN)}`, {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify({ query, variables }),
+    signal: controller.signal,
   });
+  clearTimeout(id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 39 - 43, Update the fetch request in fetch-incidents.mjs and the
corresponding request in append-to-sheet.mjs to use an AbortController with a
finite timeout, passing its signal to fetch and ensuring the timer is cleaned up
after completion. Preserve the existing request behavior while preventing
stalled external calls from hanging indefinitely.

53-70: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject all unsuccessful HTTP responses.

Currently, if the AppSignal API returns a non-401 error with a JSON body that lacks a body.errors array (e.g., a custom 429 Too Many Requests or a 5xx response), the script will silently return body.data (which is likely undefined), leading to downstream failures.

Check res.ok to catch all HTTP-level failures before returning.

💡 Proposed fix
         '  4. Has the token been regenerated since you copied it?'
     );
   }
+  if (!res.ok) {
+    die(`HTTP ${res.status}: ${JSON.stringify(body).slice(0, 500)}`);
+  }
   if (body.errors) die(`GraphQL: ${JSON.stringify(body.errors).slice(0, 500)}`);
   return body.data;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 53 - 70, Update the response handling before the existing
authentication and GraphQL error checks to reject every HTTP response where
res.ok is false, including responses without body.errors. Preserve the current
detailed handling for 401/authentication failures, then use die with the HTTP
status and available response message for other unsuccessful responses; only
return body.data after all HTTP-level and GraphQL checks pass.
plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs (2)

204-213: 🗄️ Data Integrity & Integration | 🟠 Major

Preserve existing values when recurrence fields are omitted.

Omitted fields in the payload (like sample_count or http_status) will evaluate to '' and overwrite the existing values in the sheet. Only include fields in the patch object if they are present in the payload, and use nullish checks to ensure valid 0 values are retained.

Proposed fix
         const patch = {
-          last_seen: row.last_seen || '',
           last_run_date: today,
           times_seen: prev + 1,
-          sample_count: row.sample_count || '',
-          incident_count: row.incident_count || '',
-          org_count: row.org_count || '',
-          org_ids: row.org_ids || '',
-          http_status: row.http_status || '',
         };
+        if (row.last_seen != null) patch.last_seen = row.last_seen;
+        if (row.sample_count != null) patch.sample_count = row.sample_count;
+        if (row.incident_count != null) patch.incident_count = row.incident_count;
+        if (row.org_count != null) patch.org_count = row.org_count;
+        if (row.org_ids != null) patch.org_ids = row.org_ids;
+        if (row.http_status != null) patch.http_status = row.http_status;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 204 - 213, Update the patch construction in the recurrence update
flow to include optional recurrence fields only when present in the payload,
preserving existing sheet values when they are omitted. Replace truthiness
fallbacks for fields such as sample_count, incident_count, org_count, org_ids,
and http_status with nullish checks so valid 0 values are retained, and
conditionally add omitted fields rather than assigning empty strings.

214-234: 🔒 Security & Privacy | 🟠 Major

Escape untrusted Sheet values before writing them.

AppSignal fields are written raw via both setValue() and setValues(). Any string starting with = will be executed as an unintended formula in Google Sheets. Prefix untrusted string values with an apostrophe (') before writing them to the sheet to prevent formula injection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 214 - 234, Sanitize all untrusted string values before writing them
to the sheet in both the patch update loop and the pending-row construction.
Update the setValue path and the values generated by COLUMNS.map to prefix
strings beginning with “=” (or otherwise use the established apostrophe
escaping) so they are stored as text rather than formulas, while preserving
non-string values and existing defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`:
- Around line 125-128: Fail closed when no token is configured by updating the
token checks in doGet (Code.gs lines 125-128) and doPost (Code.gs lines 162-165)
to reject requests unless the expected token exists and matches params.token,
using the strict !token || params.token !== token validation in both sites.
- Around line 72-88: In Code.gs lines 72-88, update the sheet initialization
flow to expand columns with insertColumnsAfter when sh.getMaxColumns() is below
COLUMNS.length, before reading or writing the header. In Code.gs lines 232-234,
expand rows with insertRowsAfter when sh.getMaxRows() is below sh.getLastRow() +
pending.length, before batch insertion; both sites require direct changes.
- Around line 195-197: Update the batch processing logic around the pendingIds
guard and existing-incident update path to track every processed ID, not only
newly queued rows. Record an ID after successfully handling either a new or
persisted incident, while preserving first-occurrence-wins behavior so duplicate
existing IDs are not updated or counted twice.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`:
- Around line 250-261: The fetch-incidents flow currently limits results to 100
without handling a full page. Add pagination using the GraphQL response’s cursor
fields to retrieve all incidents before calling toSignatureRows, or explicitly
warn/fail when data.app.exceptionIncidents reaches 100; ensure the watermark
cannot advance while incidents remain unfetched.

---

Outside diff comments:
In `@plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md`:
- Line 29: Update the triage-flow-webhooks documentation to consistently use
signature-based deduplication terminology: replace incident_id and incidentIds
references with signature_key and signatureKeys in the deduplication
description, return documentation, filtering guidance, recurring-incident
payload guidance, server-side deduplication note, and final lookup instruction.

---

Duplicate comments:
In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`:
- Around line 204-213: Update the patch construction in the recurrence update
flow to include optional recurrence fields only when present in the payload,
preserving existing sheet values when they are omitted. Replace truthiness
fallbacks for fields such as sample_count, incident_count, org_count, org_ids,
and http_status with nullish checks so valid 0 values are retained, and
conditionally add omitted fields rather than assigning empty strings.
- Around line 214-234: Sanitize all untrusted string values before writing them
to the sheet in both the patch update loop and the pending-row construction.
Update the setValue path and the values generated by COLUMNS.map to prefix
strings beginning with “=” (or otherwise use the established apostrophe
escaping) so they are stored as text rather than formulas, while preserving
non-string values and existing defaults.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`:
- Around line 39-43: Update the fetch request in fetch-incidents.mjs and the
corresponding request in append-to-sheet.mjs to use an AbortController with a
finite timeout, passing its signal to fetch and ensuring the timer is cleaned up
after completion. Preserve the existing request behavior while preventing
stalled external calls from hanging indefinitely.
- Around line 53-70: Update the response handling before the existing
authentication and GraphQL error checks to reject every HTTP response where
res.ok is false, including responses without body.errors. Preserve the current
detailed handling for 401/authentication failures, then use die with the HTTP
status and available response message for other unsuccessful responses; only
return body.data after all HTTP-level and GraphQL checks pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 680ceeed-b4df-4cd3-9b22-08658632dcc2

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfc3bd and 8bdee8c.

📒 Files selected for processing (8)
  • plugins/flow-webhook-triage/README.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/SKILL.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/references/sheet-setup.md
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/append-to-sheet.mjs
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/test-upsert.js
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/test-redact.mjs

Comment on lines +72 to +88
if (sh.getLastRow() === 0) {
sh.getRange(1, 1, 1, COLUMNS.length).setValues([COLUMNS]);
sh.setFrozenRows(1);
return sh;
}

// The header is NOT write-once. Rows are written positionally against COLUMNS, so a
// header left over from an older deployment silently mislabels every column — the data
// is right and every name is wrong, which reads fine and is therefore worse than an
// error. (This happened for real: a 24-column header survived a change to 28 columns,
// leaving `occurrences` above sample_count and `org_id` above incident_count.)
const header = sh.getRange(1, 1, 1, COLUMNS.length).getValues()[0];
if (COLUMNS.some((c, i) => header[i] !== c)) {
sh.getRange(1, 1, 1, COLUMNS.length).setValues([COLUMNS]);
sh.setFrozenRows(1);
}
return sh;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Auto-expand sheet dimensions before writing.

Unlike appendRow(), getRange() and setValues() do not automatically expand a sheet's dimensions. If a requested range exceeds the physical grid, it throws a fatal The coordinates or dimensions of the range are invalid exception. Google Sheets creates new sheets with 26 columns, while COLUMNS.length is 28, guaranteeing a crash on a fresh deployment. Similarly, bulk inserting rows can exceed getMaxRows().

  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L72-L88: Ensure sh.getMaxColumns() is at least COLUMNS.length, expanding it via sh.insertColumnsAfter() if necessary, before reading or writing the header.
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L232-L234: Ensure sh.getMaxRows() is at least sh.getLastRow() + pending.length, expanding it via sh.insertRowsAfter() if necessary, before batch inserting the new rows.
📍 Affects 1 file
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L72-L88 (this comment)
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L232-L234
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 72 - 88, In Code.gs lines 72-88, update the sheet initialization
flow to expand columns with insertColumnsAfter when sh.getMaxColumns() is below
COLUMNS.length, before reading or writing the header. In Code.gs lines 232-234,
expand rows with insertRowsAfter when sh.getMaxRows() is below sh.getLastRow() +
pending.length, before batch insertion; both sites require direct changes.

Comment on lines +125 to +128
const token = expectedToken_();
if (token && params.token !== token) {
return json_({ error: 'unauthorized' });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major

Fail closed when TRIAGE_TOKEN is not configured.

The public Web App still grants unauthenticated access whenever the token script property is accidentally omitted, as token evaluates to falsy and the authorization check is bypassed.

  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L125-L128: Update the doGet validation to reject requests unless a configured token is present and matches (if (!token || params.token !== token)).
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L162-L165: Apply the same strict token validation in doPost.
📍 Affects 1 file
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L125-L128 (this comment)
  • plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs#L162-L165
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 125 - 128, Fail closed when no token is configured by updating the
token checks in doGet (Code.gs lines 125-128) and doPost (Code.gs lines 162-165)
to reject requests unless the expected token exists and matches params.token,
using the strict !token || params.token !== token validation in both sites.

Comment on lines +195 to +197
// Same signature twice in one batch: the first wins. Must be checked before the
// `index` lookup — a queued row has no row number to update yet.
if (pendingIds[id]) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major

Deduplicate existing incidents within the current batch.

This check skips subsequent occurrences of an ID, but it relies on pendingIds which is only updated for new rows on line 227. If an already-persisted ID appears twice in the same request, it will skip this guard, update the existing row twice, and increment times_seen twice. Track every processed ID regardless of whether it's new or existing.

Proposed fix
       // Same signature twice in one batch: the first wins. Must be checked before the
       // `index` lookup — a queued row has no row number to update yet.
       if (pendingIds[id]) continue;
+      pendingIds[id] = true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/apps-script/Code.gs`
around lines 195 - 197, Update the batch processing logic around the pendingIds
guard and existing-incident update path to track every processed ID, not only
newly queued rows. Record an ID after successfully handling either a new or
persisted incident, while preserving first-occurrence-wins behavior so duplicate
existing IDs are not updated or counted twice.

Comment on lines +250 to +261
const data = await gql(QUERY, {
appId: APP_ID,
namespaces: NAMESPACES,
limit: 100,
start,
end: null,
sampleLimit: SAMPLES_PER_INCIDENT,
});

const incidents = (data?.app?.exceptionIncidents || []).filter((i) => (i.samples || []).length);
const rows = toSignatureRows(incidents);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Avoid silent truncation of incidents when the 100-item limit is reached.

The GraphQL query requests a maximum of 100 incidents. If the time window contains more than 100 incidents, the remaining ones are silently dropped, causing them to be permanently skipped as the watermark advances.

Either add pagination using the API's cursor fields to fetch all incidents, or fail/warn explicitly when data.app.exceptionIncidents.length === 100 so the user knows they are missing data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugins/flow-webhook-triage/skills/triage-flow-webhooks/scripts/fetch-incidents.mjs`
around lines 250 - 261, The fetch-incidents flow currently limits results to 100
without handling a full page. Add pagination using the GraphQL response’s cursor
fields to retrieve all incidents before calling toSignatureRows, or explicitly
warn/fail when data.app.exceptionIncidents reaches 100; ensure the watermark
cannot advance while incidents remain unfetched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant