Skip to content

✨ server: implement chat hook and worker - #1252

Open
cruzdanilo wants to merge 6 commits into
hookfrom
whatsapp
Open

✨ server: implement chat hook and worker#1252
cruzdanilo wants to merge 6 commits into
hookfrom
whatsapp

Conversation

@cruzdanilo

@cruzdanilo cruzdanilo commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added WhatsApp chat support with message verification, validation, routing, and queued processing.
    • Added automated chat responses for welcome, help, language, safety, and noise scenarios.
    • Added chat evaluation tooling to assess response quality, tool usage, links, costs, and token consumption.
  • Improvements
    • Enhanced error reporting and transaction naming for Cloud Run services.
    • Added deployment support for chat services and updated branch-based deployment triggers.
  • Quality
    • Expanded automated coverage for chat, WhatsApp messaging, queues, workers, and monitoring.

@cruzdanilo
cruzdanilo requested a review from nfmelendez as a code owner August 24, 2026 08:15
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f0aeafd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@exactly/infra Minor
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds WhatsApp chat webhook ingestion, queue-backed processing, Mastra worker responses, runtime entrypoints, Sentry configuration, deployment wiring, and automated tests and evaluations.

Changes

Chat runtime

Layer / File(s) Summary
Chat contracts and runtime wiring
server/workers/chat/job.ts, server/workers/chat/queue.ts, server/package.json, infra/utils/modules.ts, infra/Pulumi.*.yaml, package.json, pnpm-workspace.yaml, server/vitest.config.mts, cspell.json
Defines chat job data, queue behavior, package exports, Mastra and Sentry dependencies, secret configuration, phone-number identifiers, and shared test settings.
Webhook ingress and hook bootstrap
server/hooks/chat.ts, server/hooks/bin/chat.ts, server/test/hooks/*
Adds webhook verification, HMAC validation, payload parsing, message batching, queue submission, cleanup, and hook bootstrap coverage.
Chat worker and WhatsApp delivery
server/workers/chat/*, server/utils/whatsapp.ts, server/workers/chat/bin.ts, server/test/workers/*, server/test/utils/whatsapp.test.ts
Adds WhatsApp delivery and a Mastra-backed worker with sender state, localization, scripted replies, retries, supervision, integration tests, and evaluation coverage.
Runtime observability and release wiring
server/instrument.cjs, server/test/instrument.test.ts, .github/workflows/server-base-sepolia.yaml, .changeset/*
Adds Cloud Run service names to Sentry transactions, updates Sentry tracing options, changes the deployment branch trigger, and records package releases.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to d314f

The chat hook and worker can reject valid inbound messages, retain sender identifiers indefinitely, expose customer data in failure telemetry, and send duplicate replies after retries; partial startup failures can also leave clients open. These correctness, privacy, delivery, and cleanup issues make the PR unsafe to merge until fixed or explicitly accepted.

Sequence Diagram(s)

sequenceDiagram
  participant WhatsAppWebhook
  participant ChatHook
  participant ChatQueue
  participant ChatWorker
  participant MastraAgents
  participant WhatsAppGraphAPI
  WhatsAppWebhook->>ChatHook: Deliver verified webhook event
  ChatHook->>ChatQueue: Enqueue grouped chat job
  ChatQueue->>ChatWorker: Deliver chat job
  ChatWorker->>MastraAgents: Generate welcome or help response
  ChatWorker->>WhatsAppGraphAPI: Send localized text response
Loading

Suggested reviewers: nfmelendez, dieguezguille, franm91

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: implementing the server chat hook and worker.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch whatsapp
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch whatsapp

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: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e311af71-bdf1-4969-86fd-56614006652d

📥 Commits

Reviewing files that changed from the base of the PR and between 7e9b088 and e398df1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • .changeset/chubby-melons-look.md
  • .changeset/lucky-parrots-chat.md
  • .changeset/nice-days-deny.md
  • .changeset/public-vans-shine.md
  • .github/workflows/server-base-sepolia.yaml
  • cspell.json
  • infra/utils/modules.ts
  • package.json
  • pnpm-workspace.yaml
  • server/hooks/bin/chat.ts
  • server/hooks/chat.ts
  • server/instrument.cjs
  • server/package.json
  • server/test/hooks/bin.test.ts
  • server/test/hooks/chat.test.ts
  • server/test/hooks/hook.test.ts
  • server/test/instrument.test.ts
  • server/test/utils/whatsapp.test.ts
  • server/test/workers/bin.test.ts
  • server/test/workers/chat.eval.ts
  • server/test/workers/chat.test.ts
  • server/utils/whatsapp.ts
  • server/workers/chat/bin.ts
  • server/workers/chat/job.ts
  • server/workers/chat/queue.ts
  • server/workers/chat/worker.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread server/hooks/chat.ts
Comment on lines +82 to +84
.catch((error: unknown) => {
captureException(error, { extra: { sender }, tags: { job: "chat", queue: "chat" } });
throw error;

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

🧩 Analysis chain

🌐 Web query:

How does the Sentry JavaScript SDK include captureException extra data in captured events?

💡 Result:

The Sentry JavaScript SDK provides two primary ways to include extra data when calling captureException: passing it directly as an argument or utilizing scope management [1][2][3]. Direct Inclusion via captureException You can include extra data directly in a single captureException call by passing a capture context object as the second argument [1][4][5]. This object accepts an extra property, which should be a key-value record of additional data [1][4]. Example: Sentry.captureException(error, { extra: { userId: 123, environment: 'production' } }); This data will be merged with existing data in the current scope [5][3]. Scope Management Alternatively, you can manage extra data using the SDK's scope-based methods. This is useful when you want to persist data across multiple events or limit the scope of the data to a specific block of code [2][3]. 1. Global/Current Scope Methods: You can use setExtra or setExtras to add data that will be included in all future events captured on that scope [1][6][7]. Sentry.setExtra('key', 'value'); Sentry.setExtras({ key1: 'value1', key2: 'value2' }); 2. Using withScope: To isolate extra data to a specific event or block of code, you can use withScope [2][8]. This creates a temporary scope that is automatically cleaned up after the callback executes [3]. Sentry.withScope((scope) => { scope.setExtra('key', 'value'); Sentry.captureException(error); }); While the SDK continues to support extra data for backward compatibility, Sentry documentation notes that unstructured "additional data" via setExtra is largely deprecated in favor of more structured Contexts (using setContext), which provide better organization in the Sentry UI [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- production hook ---'
sed -n '1,180p' server/hooks/chat.ts
printf '%s\n' '--- affected test sections ---'
sed -n '200,285p' server/test/hooks/chat.test.ts
printf '%s\n' '--- captureException definitions and usages ---'
rg -n --glob '*.{ts,tsx,js,jsx}' 'captureException|enqueue|sender' server

Repository: exactly/exa

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- Sentry configuration ---'
rg -n -C 4 --glob '*.{ts,tsx,js,jsx}' 'Sentry\.init|beforeSend|beforeBreadcrumb|sendDefaultPii|normalizeDepth|extra' server . --glob '!node_modules/**' --glob '!dist/**' | head -n 240
printf '%s\n' '--- additional chat test context ---'
sed -n '1,90p' server/test/hooks/chat.test.ts
sed -n '240,275p' server/test/hooks/chat.test.ts
printf '%s\n' '--- focused structural probe ---'
python3 - <<'PY'
from pathlib import Path

hook = Path("server/hooks/chat.ts").read_text()
test = Path("server/test/hooks/chat.test.ts").read_text()

required_sender_fields = ["id", "from", "text", "contact", "phoneNumberId"]
missing = [field for field in required_sender_fields if field not in hook]
assert not missing, f"missing sender fields in production source: {missing}"
assert 'extra: { sender }' in hook, "production source does not pass sender as Sentry extra"
assert 'extra: { sender: { id: "whatsapp-1", from: "US.12345678", text: "Hi!", contact: "Jhon", phoneNumberId: "321" } }' in test, "test does not assert the complete sender payload"

print("production sender fields:", ", ".join(required_sender_fields))
print("production Sentry extra: complete sender object")
print("test expectation: complete sender object")
PY

Repository: exactly/exa

Length of output: 19406


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- instrumentation candidates ---'
fd -HI -t f '(instrument|sentry|config)' . | head -n 120
printf '%s\n' '--- Sentry initialization ---'
rg -n -C 8 --glob '*.{ts,tsx,js,jsx}' 'init\s*\(|beforeSend|beforeSendTransaction|sendDefaultPii|Sentry' . --glob '!node_modules/**' --glob '!dist/**' | head -n 260
printf '%s\n' '--- relevant diff summary ---'
git diff --stat -- server/hooks/chat.ts server/test/hooks/chat.test.ts
git diff --unified=3 -- server/hooks/chat.ts server/test/hooks/chat.test.ts | sed -n '1,220p'

Repository: exactly/exa

Length of output: 17919


🏁 Script executed:

#!/bin/bash
set -eu
cat -n server/instrument.cjs
printf '%s\n' '--- Sentry mock ---'
cat -n server/test/mocks/sentry.ts

Repository: exactly/exa

Length of output: 2730


Remove customer content from Sentry event extras. sender includes contact, the WhatsApp user ID, and message text. beforeSend does not redact these fields before Sentry receives the event.

  • server/hooks/chat.ts#L82-L84: pass only allowlisted diagnostics, such as id and phoneNumberId.
  • server/test/hooks/chat.test.ts#L228-L237: assert the redacted payload.
📍 Affects 2 files
  • server/hooks/chat.ts#L82-L84 (this comment)
  • server/test/hooks/chat.test.ts#L228-L237

Comment thread server/hooks/chat.ts
Comment on lines +102 to +143
function parse({ entry }: v.InferOutput<typeof event>) {
return entry.flatMap(({ changes }) =>
changes.flatMap(({ value: { contacts, messages, metadata } }) =>
(messages ?? []).flatMap((message) =>
message.text
? [
{
id: message.id,
from: message.from_user_id,
text: message.text.body,
contact: contacts?.find(({ user_id }) => user_id === message.from_user_id)?.profile?.name,
phoneNumberId: metadata.phone_number_id,
},
]
: [],
),
),
);
}

const event = v.object({
entry: v.array(
v.object({
changes: v.array(
v.object({
value: v.object({
metadata: v.object({ phone_number_id: v.string() }),
contacts: v.optional(
v.array(
v.object({
user_id: v.string(),
profile: v.optional(v.object({ name: v.optional(v.string()) })),
}),
),
),
messages: v.optional(
v.array(
v.object({
id: v.string(),
from_user_id: v.string(),
type: v.string(),
text: v.optional(v.object({ body: v.string() })),

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

🌐 Web query:

What fields identify the sender and contact in Meta WhatsApp Cloud API inbound message webhooks?

💡 Result:

In Meta WhatsApp Cloud API inbound message webhooks, the sender and contact information are identified primarily through the following fields located within the entries object of the JSON payload [1][2]: Sender Identification The sender of the message is identified within the messages array object by the from field [1][3]. This field contains the WhatsApp ID (wa_id) of the user who sent the message, which is typically their phone number [1][2]. Contact Identification Additional contact details, such as the user's profile name, are provided in the contacts array, which is present at the same level as the messages array within the webhook value object [1][4][2]. Each entry in the contacts array typically includes: wa_id: The WhatsApp ID of the user, which corresponds to the from field in the message object [1][2]. profile: An object containing user-specific information, such as the name field, which provides the sender's WhatsApp profile name [1][2]. These fields allow you to link the incoming message (via the from field) to the detailed contact information (via the wa_id and profile fields) provided in the same webhook notification [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- production hook ---'
sed -n '70,155p' server/hooks/chat.ts
printf '%s\n' '--- test fixture and assertions ---'
sed -n '210,285p' server/test/hooks/chat.test.ts
printf '%s\n' '--- field references ---'
rg -n '"(user_id|wa_id|from_user_id|from)"|user_id|wa_id|from_user_id' server/hooks/chat.ts server/test/hooks/chat.test.ts

Repository: exactly/exa

Length of output: 6092


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

hook = Path("server/hooks/chat.ts").read_text()
fixture = Path("server/test/hooks/chat.test.ts").read_text()

checks = {
    "production schema uses user_id": "user_id: v.string()" in hook,
    "production schema uses from_user_id": "from_user_id: v.string()" in hook,
    "production schema uses wa_id": "wa_id" in hook,
    "production schema uses from field": "from: v.string()" in hook,
    "test fixture uses user_id": "user_id:" in fixture,
    "test fixture uses from_user_id": "from_user_id:" in fixture,
    "test fixture uses wa_id": "wa_id:" in fixture,
    "test fixture uses from field": "from:" in fixture,
}

for name, value in checks.items():
    print(f"{name}: {value}")

# Model the relevant webhook transformation without importing repository code.
payload = {
    "entry": [{
        "changes": [{
            "value": {
                "metadata": {"phone_number_id": "321"},
                "contacts": [{"wa_id": "US.12345678", "profile": {"name": "Jhon"}}],
                "messages": [{
                    "id": "whatsapp-1",
                    "from": "US.12345678",
                    "type": "text",
                    "text": {"body": "Hi!"},
                }],
            },
        }],
    }],
}

value = payload["entry"][0]["changes"][0]["value"]
message = value["messages"][0]
schema_expected = {
    "contact identifier": value["contacts"][0]["wa_id"],
    "sender identifier": message["from"],
}
schema_current = {
    "contact identifier": value["contacts"][0].get("user_id"),
    "sender identifier": message.get("from_user_id"),
}
print("real webhook fields:", schema_expected)
print("current schema lookups:", schema_current)
PY

Repository: exactly/exa

Length of output: 615


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,75p' server/hooks/chat.ts
printf '%s\n' '--- all webhook parsing and validation calls ---'
rg -n -C 3 'parse\(|event|safeParse|parse\(' server/hooks/chat.ts server/test/hooks/chat.test.ts

Repository: exactly/exa

Length of output: 5928


Use the WhatsApp Cloud API field names. The schema requires contacts[].user_id and messages[].from_user_id, but inbound WhatsApp webhooks provide contacts[].wa_id and messages[].from. Text messages therefore fail validation before queue.enqueue.

  • server/hooks/chat.ts:102-143: validate and use wa_id and from.
  • server/test/hooks/chat.test.ts:248-269: update the fixture to use wa_id and from.
📍 Affects 2 files
  • server/hooks/chat.ts#L102-L143 (this comment)
  • server/test/hooks/chat.test.ts#L248-L269

vi.stubEnv("K_SERVICE", "sandbox-chat");
const transaction = { transaction: "POST /" };

const result = config.beforeSendTransaction?.(transaction as never, {} as never);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target file ---'
cat -n server/test/instrument.test.ts
printf '%s\n' '--- package manifests and sentry references ---'
rg -n --glob 'package.json' --glob 'pnpm-lock.yaml' --glob 'yarn.lock' --glob 'package-lock.json' '`@sentry/`(core|node)|sentry' .
printf '%s\n' '--- callback and fixture references ---'
rg -n 'beforeSendTransaction|EventHint|TransactionEvent|as never|type: "transaction"' server

Repository: exactly/exa

Length of output: 20998


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- instrument implementation ---'
cat -n server/instrument.cjs
printf '%s\n' '--- relevant TypeScript configuration ---'
fd -HI 'tsconfig*.json' -o -HI 'package.json' | sort | xargs -r grep -nH -E 'allowJs|checkJs|strict|typecheck|tsc|vitest'
printf '%s\n' '--- Sentry event types at the declared lockfile version ---'
curl -fsSL https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/packages/core/src/types/event.ts |
  rg -n -A18 -B8 'interface TransactionEvent|type TransactionEvent|interface EventHint|type EventHint|BeforeSendTransaction'

Repository: exactly/exa

Length of output: 3497


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- project configuration ---'
find . -maxdepth 3 -type f \( -name 'tsconfig.json' -o -name 'tsconfig.*.json' \) -print
cat -n server/package.json | sed -n '1,115p'
printf '%s\n' '--- callback type definitions ---'
for url in \
  https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/packages/core/src/types-hoist/options.ts \
  https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/packages/core/src/types-hoist/event.ts \
  https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/packages/core/src/types/event.ts
do
  echo "--- $url"
  curl -fsSL "$url" | rg -n -A12 -B8 'beforeSendTransaction|ProcessEventTransaction|TransactionEvent'
done
printf '%s\n' '--- available compiler ---'
command -v tsc || true
tsc --version 2>/dev/null || true

Repository: exactly/exa

Length of output: 5367


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- TypeScript configuration ---'
for file in tsconfig.json server/tsconfig.json; do
  echo "--- $file"
  cat -n "$file"
done
printf '%s\n' '--- Sentry core source tree candidates ---'
curl -fsSL 'https://api.github.com/repos/getsentry/sentry-javascript/git/trees/10.70.0?recursive=1' |
  jq -r '.tree[].path' |
  rg 'packages/(core|node)/.*(options|types|client).*\.ts$' |
  head -80
printf '%s\n' '--- callback declarations in the v10.70.0 tree ---'
paths=$(curl -fsSL 'https://api.github.com/repos/getsentry/sentry-javascript/git/trees/10.70.0?recursive=1' |
  jq -r '.tree[].path' |
  rg '^packages/(core|node)/.*\.ts$' |
  rg '(options|types|client|baseclient)')
while IFS= read -r path; do
  content=$(curl -fsSL "https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/$path") || continue
  if printf '%s' "$content" | rg -q 'beforeSendTransaction|ProcessEventTransaction|TransactionEvent'; then
    echo "--- $path"
    printf '%s' "$content" | rg -n -A12 -B8 'beforeSendTransaction|ProcessEventTransaction|TransactionEvent'
  fi
done <<< "$paths"

Repository: exactly/exa

Length of output: 33989


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- public exports ---'
for path in packages/core/src/index.ts packages/core/src/types/index.ts packages/core/package.json; do
  echo "--- $path"
  curl -fsSL "https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/$path" |
    rg -n -A8 -B4 'EventHint|TransactionEvent|types/event'
done
printf '%s\n' '--- event base shape ---'
curl -fsSL https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/packages/core/src/types/event.ts |
  sed -n '1,90p'
printf '%s\n' '--- compiler availability ---'
command -v tsc || true
tsc --version 2>/dev/null || true

Repository: exactly/exa

Length of output: 203


🏁 Script executed:

#!/bin/bash
set +e
printf '%s\n' '--- package metadata ---'
curl -fsSL https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/packages/core/package.json |
  sed -n '1,180p'
printf '%s\n' '--- core source export candidates ---'
curl -fsSL 'https://api.github.com/repos/getsentry/sentry-javascript/git/trees/10.70.0?recursive=1' |
  jq -r '.tree[].path' |
  rg '^packages/core/src/.*(index|types).*\.ts$'
printf '%s\n' '--- exports containing event types ---'
for path in \
  packages/core/src/index.ts \
  packages/core/src/types.ts \
  packages/core/src/types/index.ts \
  packages/core/src/types-hoist.ts \
  packages/core/src/types-hoist/index.ts
do
  echo "--- $path"
  body=$(curl -fsSL "https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/$path")
  status=$?
  if [ "$status" -eq 0 ]; then
    printf '%s\n' "$body" | rg -n -A6 -B4 'EventHint|TransactionEvent|types/event|export \*' || true
  else
    echo "unavailable"
  fi
done
printf '%s\n' '--- compiler availability ---'
command -v tsc || true
tsc --version 2>/dev/null || true

Repository: exactly/exa

Length of output: 7542


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Sentry public export lists ---'
for path in packages/core/src/shared-exports.ts packages/core/src/server-exports.ts packages/core/src/browser-exports.ts; do
  echo "--- $path"
  curl -fsSL "https://raw.githubusercontent.com/getsentry/sentry-javascript/10.70.0/$path" |
    rg -n -A5 -B3 'event|EventHint|TransactionEvent|export'
done
printf '%s\n' '--- standalone TypeScript fixture probe ---'
tmp=$(mktemp --suffix=.ts)
trap 'rm -f "$tmp"' EXIT
cat >"$tmp" <<'TS'
interface Event {
  type?: "transaction" | "profile" | "replay_event" | "feedback";
  transaction?: string;
  extra?: Record<string, unknown>;
}
interface TransactionEvent extends Event {
  type: "transaction";
}
interface EventHint {
  originalException?: unknown;
}
declare const beforeSendTransaction:
  | ((event: TransactionEvent, hint: EventHint) => TransactionEvent | null)
  | undefined;

const transaction = { type: "transaction", transaction: "POST /" } satisfies TransactionEvent;
const ignored = { type: "transaction", extra: { "exa.ignore": true }, transaction: "POST /" } satisfies TransactionEvent;
const hint = {} satisfies EventHint;
transaction.transaction = "POST / · sandbox-chat";
ignored.transaction = "POST /";
const result = beforeSendTransaction?.(transaction, hint);
TS
tsc --strict --noEmit --skipLibCheck "$tmp"

Repository: exactly/exa

Length of output: 27804


🏁 Script executed:

#!/bin/bash
set -e
tmp=$(mktemp --suffix=.ts)
trap 'rm -f "$tmp"' EXIT
cat >"$tmp" <<'TS'
interface Event {
  type?: "transaction" | "profile" | "replay_event" | "feedback";
  transaction?: string;
  extra?: Record<string, unknown>;
}
interface TransactionEvent extends Event {
  type: "transaction";
}
interface EventHint {
  originalException?: unknown;
}
declare const beforeSendTransaction:
  | ((event: TransactionEvent, hint: EventHint) => TransactionEvent | null)
  | undefined;

const transaction = { type: "transaction", transaction: "POST /" } satisfies TransactionEvent;
const ignored = { type: "transaction", extra: { "exa.ignore": true }, transaction: "POST /" } satisfies TransactionEvent;
const hint = {} satisfies EventHint;
transaction.transaction = "POST / · sandbox-chat";
ignored.transaction = "POST /";
const result = beforeSendTransaction?.(transaction, hint);
TS
tsc --ignoreConfig --strict --noEmit --skipLibCheck "$tmp"

Repository: exactly/exa

Length of output: 1224


🏁 Script executed:

#!/bin/bash
set -e
tmp=$(mktemp --suffix=.ts)
trap 'rm -f "$tmp"' EXIT
cat >"$tmp" <<'TS'
interface SentryEvent {
  type?: "transaction" | "profile" | "replay_event" | "feedback";
  transaction?: string;
  extra?: Record<string, unknown>;
}
interface TransactionEvent extends SentryEvent {
  type: "transaction";
}
interface EventHint {
  originalException?: unknown;
}
declare const beforeSendTransaction:
  | ((event: TransactionEvent, hint: EventHint) => TransactionEvent | null)
  | undefined;

const transaction = { type: "transaction", transaction: "POST /" } satisfies TransactionEvent;
const ignored = { type: "transaction", extra: { "exa.ignore": true }, transaction: "POST /" } satisfies TransactionEvent;
const hint = {} satisfies EventHint;
transaction.transaction = "POST / · sandbox-chat";
ignored.transaction = "POST /";
const result = beforeSendTransaction?.(transaction, hint);
TS
tsc --ignoreConfig --strict --noEmit --skipLibCheck --lib ESNext "$tmp"

Repository: exactly/exa

Length of output: 149


Use typed Sentry fixtures instead of never casts.

Add type: "transaction" to each fixture. Use EventHint and TransactionEvent from @sentry/core. Call beforeSendTransaction without casts.

Source: Coding guidelines

Comment on lines +60 to +65
const seen = await bullmq.exists(`whatsapp:seen:${data.from}`);
const { text } = await reply(data.text, {
requestContext: new RequestContext<InferPublicSchema<typeof context>>([["seen", seen === 1]]),
});
await whatsapp.send(data.from, text);
await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "NX");

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

Set an expiry on the whatsapp:seen: key.

bullmq.set(\whatsapp:seen:${data.from}`, ..., "NX")` writes a key with no TTL. The key name contains the sender phone number. Every sender that ever writes creates one permanent Redis key. This grows without bound and retains a personal identifier indefinitely.

Add an expiry that matches the intended "returning sender" window.

🔒️ Proposed fix
-        await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "NX");
+        await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "EX", seenTtl, "NX");

Add the constant next to ttl:

const seenTtl = 90 * 24 * 3600;
📝 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 seen = await bullmq.exists(`whatsapp:seen:${data.from}`);
const { text } = await reply(data.text, {
requestContext: new RequestContext<InferPublicSchema<typeof context>>([["seen", seen === 1]]),
});
await whatsapp.send(data.from, text);
await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "NX");
const seenTtl = 90 * 24 * 3600;
const seen = await bullmq.exists(`whatsapp:seen:${data.from}`);
const { text } = await reply(data.text, {
requestContext: new RequestContext<InferPublicSchema<typeof context>>([["seen", seen === 1]]),
});
await whatsapp.send(data.from, text);
await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "EX", seenTtl, "NX");

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e398df1e9f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

import type { Redis } from "ioredis";

export default function chat(bullmq: Redis) {
const queue = createQueue<Job>(name, attempts, bullmq);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain completed message IDs for webhook deduplication

This queue inherits removeOnComplete: true from server/workers/queue.ts, so the WhatsApp message ID stops providing deduplication immediately after processing. If a multi-sender webhook partially enqueues and then returns 500 because another enqueue failed—or if the 200 response is lost—a later webhook retry can re-enqueue an already completed message and send the user a duplicate reply. Retain completed jobs for the webhook retry window or store processed message IDs separately.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 885d52f342

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/hooks/chat.ts
v.array(
v.object({
id: v.string(),
from_user_id: v.string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate WhatsApp's actual sender fields

For actual WhatsApp Cloud API message callbacks, contacts identify the sender with wa_id and messages use from, but this schema requires user_id and from_user_id. Because the Valibot middleware runs before parse, every normal inbound text webhook is rejected with 400 bad chat; the tests currently mirror the incorrect field names instead of the external contract.

Useful? React with 👍 / 👎.

Comment thread server/utils/whatsapp.ts
body: JSON.stringify({
messaging_product: "whatsapp",
recipient_type: "individual",
recipient,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Send the destination in the to field

WhatsApp's Cloud API expects the destination number in the to request field; recipient is not the corresponding send-message field. Consequently every outbound request is rejected by the Graph API, and the worker exhausts its retries without delivering any reply.

Useful? React with 👍 / 👎.

Comment on lines +64 to +65
await whatsapp.send(data.from, text);
await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "NX");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent retries after an accepted WhatsApp send

If WhatsApp accepts the message but the response is lost, the worker terminates, or the following Redis write fails, the BullMQ process rejects and retries the entire job up to ten times, including whatsapp.send. This can send the same reply repeatedly; persist an idempotent delivery state or otherwise keep post-send bookkeeping failures from replaying the external side effect.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1ebf82c1c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/hooks/chat.ts
}

function verify(body: string, signature?: string, secret?: string) {
if (!secret) return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject an empty production app secret

If the Secret Manager version exists but contains an empty value, secret() returns "", so this branch accepts every request even when the signature header is missing. Because the chat Cloud Run service is public, that misconfiguration lets anyone submit schema-valid payloads and enqueue replies to attacker-selected numbers, consuming model/API quota and potentially sending spam; reserve unsigned mode for undefined or fail startup when the configured secret is empty.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39943df5b7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/hooks/chat.ts
Comment on lines +77 to +80
id: sender.id,
contact: sender.contact,
from: sender.from,
text: [sender, ...tail].map(({ text }) => text).join("\n"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Track every message ID in a merged thread

When a webhook batch contains multiple messages from one sender, the queued text includes every message but the job ID records only sender.id. If a retry delivers a tail message separately while the merged job is still retained, that tail ID is unknown and produces a duplicate reply; conversely, a batch whose first message already has an active job silently discards any new tail messages when BullMQ returns the existing job. Enqueue messages independently or reserve every included message ID before acknowledging the batch.

Useful? React with 👍 / 👎.

import * as v from "valibot";

import { attempts, name, type Job } from "./job";
import sentry from "../../instrument.cjs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid initializing Sentry again inside the chat worker

In the packaged worker, this local import is bundled into dist/workers/chat/bin.cjs, so evaluating it executes init(config) from server/instrument.cjs; the Docker entrypoint already preloads that same instrumentation with --require=./instrument.cjs before launching the bundle. Chat workers therefore initialize the Node SDK twice, replacing the active client and potentially installing logging/profiling instrumentation more than once. Move the reusable options into a side-effect-free module instead of importing the initializer.

Useful? React with 👍 / 👎.

);

const model = "anthropic/claude-sonnet-5";
const ttl = 3600;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Express the response-cache TTL in milliseconds

ResponseCache passes this TTL to the in-memory server cache in milliseconds, so 3600 keeps a translated script for only 3.6 seconds. Since messages from the same locale will ordinarily arrive farther apart than that, the worker repeatedly invokes the translator model—twice for every non-English help response—despite installing a response cache, adding avoidable latency and model cost. Set the value to the intended duration in milliseconds.

Useful? React with 👍 / 👎.

@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: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 16d42616-c862-4d92-bdff-ba3ee43ae201

📥 Commits

Reviewing files that changed from the base of the PR and between bd1d570 and acba01a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • package.json
  • pnpm-workspace.yaml
  • server/package.json
  • server/test/workers/chat.eval.ts
  • server/test/workers/chat.test.ts
  • server/workers/chat/worker.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment on lines +11 to +12
const model = { id: "anthropic/claude-sonnet-5", apiKey: env.ANTHROPIC_API_KEY } as const;
const { reply } = chat(env.ANTHROPIC_API_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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fail fast when ANTHROPIC_API_KEY is missing.

Line 12 substitutes "" for a missing key, and Line 11 passes undefined to the judge model. The script then runs every case and fails on each one with a provider authentication error. Validate the variable once at startup so the failure names the missing configuration.

♻️ Proposed fix
-const model = { id: "anthropic/claude-sonnet-5", apiKey: env.ANTHROPIC_API_KEY } as const;
-const { reply } = chat(env.ANTHROPIC_API_KEY ?? "");
+const apiKey = parse(pipe(string(), nonEmpty()), env.ANTHROPIC_API_KEY);
+const model = { id: "anthropic/claude-sonnet-5", apiKey } as const;
+const { reply } = chat(apiKey);

Add the valibot import:

import { nonEmpty, parse, pipe, string } from "valibot";
📝 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 model = { id: "anthropic/claude-sonnet-5", apiKey: env.ANTHROPIC_API_KEY } as const;
const { reply } = chat(env.ANTHROPIC_API_KEY ?? "");
const apiKey = parse(pipe(string(), nonEmpty()), env.ANTHROPIC_API_KEY);
const model = { id: "anthropic/claude-sonnet-5", apiKey } as const;
const { reply } = chat(apiKey);

Comment on lines +217 to +225
const ok =
judged.score === 1 && tools.length === 1 && tools[0] === called && dropped.length === 0 && broken.length === 0;
if (!ok) failed += 1;
const why = [
tools.join(", ") !== called && `expected it to call ${called} — it called ${tools.join(", ") || "no tools"}`,
...dropped.map((fragment) => `expected the reply to contain ${JSON.stringify(fragment)} — it does not`),
...broken.map((url) => `expected ${url} exactly once — it appears ${text.split(url).length - 1} times`),
judged.score !== 1 && `rubric not satisfied — ${judged.reason?.replaceAll("\n", " ")}`,
].filter((entry) => typeof entry === "string");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the tool check between ok and why.

Line 218 marks a case failed when tools.length !== 1. Line 221 emits the explanation only when tools.join(", ") !== called. These conditions agree for the current cases, but they can diverge. Reuse one derived value so the report always explains a tool mismatch.

♻️ Proposed fix
+    const calledOnce = tools.length === 1 && tools[0] === called;
     const ok =
-      judged.score === 1 && tools.length === 1 && tools[0] === called && dropped.length === 0 && broken.length === 0;
+      judged.score === 1 && calledOnce && dropped.length === 0 && broken.length === 0;
     if (!ok) failed += 1;
     const why = [
-      tools.join(", ") !== called && `expected it to call ${called} — it called ${tools.join(", ") || "no tools"}`,
+      !calledOnce && `expected it to call ${called} — it called ${tools.join(", ") || "no tools"}`,
📝 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 ok =
judged.score === 1 && tools.length === 1 && tools[0] === called && dropped.length === 0 && broken.length === 0;
if (!ok) failed += 1;
const why = [
tools.join(", ") !== called && `expected it to call ${called} — it called ${tools.join(", ") || "no tools"}`,
...dropped.map((fragment) => `expected the reply to contain ${JSON.stringify(fragment)} — it does not`),
...broken.map((url) => `expected ${url} exactly once — it appears ${text.split(url).length - 1} times`),
judged.score !== 1 && `rubric not satisfied — ${judged.reason?.replaceAll("\n", " ")}`,
].filter((entry) => typeof entry === "string");
const calledOnce = tools.length === 1 && tools[0] === called;
const ok =
judged.score === 1 && calledOnce && dropped.length === 0 && broken.length === 0;
if (!ok) failed += 1;
const why = [
!calledOnce && `expected it to call ${called} — it called ${tools.join(", ") || "no tools"}`,
...dropped.map((fragment) => `expected the reply to contain ${JSON.stringify(fragment)} — it does not`),
...broken.map((url) => `expected ${url} exactly once — it appears ${text.split(url).length - 1} times`),
judged.score !== 1 && `rubric not satisfied — ${judged.reason?.replaceAll("\n", " ")}`,
].filter((entry) => typeof entry === "string");

Comment on lines +216 to +226
const observed = createChatWorker({
anthropicKey: "anthropic",
bullmq: dedicated,
whatsapp,
});
const listener = listen.mock.calls.find(([event]) => event === "error")?.[1];
if (!listener) throw new Error("missing error listener");
listener(new Error("socket closed"));
expect(captureException).toHaveBeenCalledExactlyOnceWith(new Error("socket closed"));
await observed.close();
await dedicated.quit();

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 | 🔵 Trivial | ⚡ Quick win

Await observed.ready before you close the worker.

The test at Line 229 awaits observed.ready before close(). This test does not. observed.close() and dedicated.quit() can run while the BullMQ worker is still connecting, which makes the test flaky and can produce an unhandled rejection.

♻️ Proposed fix
     const listener = listen.mock.calls.find(([event]) => event === "error")?.[1];
     if (!listener) throw new Error("missing error listener");
     listener(new Error("socket closed"));
     expect(captureException).toHaveBeenCalledExactlyOnceWith(new Error("socket closed"));
+    await observed.ready;
     await observed.close();
     await dedicated.quit();
📝 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 observed = createChatWorker({
anthropicKey: "anthropic",
bullmq: dedicated,
whatsapp,
});
const listener = listen.mock.calls.find(([event]) => event === "error")?.[1];
if (!listener) throw new Error("missing error listener");
listener(new Error("socket closed"));
expect(captureException).toHaveBeenCalledExactlyOnceWith(new Error("socket closed"));
await observed.close();
await dedicated.quit();
const observed = createChatWorker({
anthropicKey: "anthropic",
bullmq: dedicated,
whatsapp,
});
const listener = listen.mock.calls.find(([event]) => event === "error")?.[1];
if (!listener) throw new Error("missing error listener");
listener(new Error("socket closed"));
expect(captureException).toHaveBeenCalledExactlyOnceWith(new Error("socket closed"));
await observed.ready;
await observed.close();
await dedicated.quit();

Comment on lines +64 to +65
await whatsapp.send(data.from, text);
await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "NX");

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 | 🟡 Minor | ⚡ Quick win

Record the sender before sending the message.

whatsapp.send runs before the whatsapp:seen: write. If the write fails, or if v.parse(v.string(), id) throws because id is undefined, the job fails after the message was already delivered. The worker then retries and sends a second message to the same person.

Write the key first, and gate the send on the NX result so a retry does not deliver a duplicate.

🐛 Proposed fix
-      async process({ data, id }) {
-        const seen = await bullmq.exists(`whatsapp:seen:${data.from}`);
-        const { text } = await reply(data.text, {
-          requestContext: new RequestContext<InferPublicSchema<typeof context>>([["seen", seen === 1]]),
-        });
-        await whatsapp.send(data.from, text);
-        await bullmq.set(`whatsapp:seen:${data.from}`, v.parse(v.string(), id), "NX");
-      },
+      async process({ data, id }) {
+        const key = `whatsapp:seen:${data.from}`;
+        const first = await bullmq.set(key, v.parse(v.string(), id), "EX", seenTtl, "NX");
+        const { text } = await reply(data.text, {
+          requestContext: new RequestContext<InferPublicSchema<typeof context>>([["seen", first === null]]),
+        });
+        await whatsapp.send(data.from, text);
+      },

Promise.all(
scripts[id].blocks.map(async ({ text, url }) =>
[
language === "en" || language.startsWith("en-")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare the locale without case sensitivity.

The model supplies locale, and the schema at Line 136 accepts any [\w-]{2,35} value. If the model returns EN or EN-US, this check fails and the English script goes to the translator. The person then receives a re-worded English reply and the process pays for an extra model call.

♻️ Proposed fix
-              language === "en" || language.startsWith("en-")
+              language.toLowerCase() === "en" || language.toLowerCase().startsWith("en-")
📝 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
language === "en" || language.startsWith("en-")
language.toLowerCase() === "en" || language.toLowerCase().startsWith("en-")

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d7544f97-96ca-45f6-b4f9-d95dded97c45

📥 Commits

Reviewing files that changed from the base of the PR and between acba01a and d314f14.

📒 Files selected for processing (8)
  • infra/Pulumi.base-sepolia.yaml
  • infra/Pulumi.sandbox.yaml
  • infra/utils/modules.ts
  • server/hooks/bin/chat.ts
  • server/test/hooks/bin.test.ts
  • server/test/workers/bin.test.ts
  • server/vitest.config.mts
  • server/workers/chat/bin.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread server/hooks/bin/chat.ts
Comment on lines +14 to +25
Promise.all([
secret("redis-url", secrets).then((redisUrl) => connect(redisUrl)),
Promise.resolve(parse(pipe(string("whatsapp id"), nonEmpty("whatsapp id")), env.WHATSAPP_PHONE_NUMBER_ID)),
secret("chat-whatsapp-app-secret", secrets),
secret("chat-whatsapp-verify-token", secrets),
]).then(([bullmq, whatsappFrom, whatsappSecret, whatsappVerifyToken]) =>
own(
createChatHook({ bullmq, whatsappFrom, whatsappSecret, whatsappVerifyToken }),
() => bullmq.quit(),
() => secrets.close(),
),
),

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 | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/exactly-exa-f26ca19b -mindepth 2 -maxdepth 2 -type f -name '*.md' -print -exec sh -c 'head -80 "$1"' _ {} \;
printf '%s\n' '--- target files ---'
sed -n '1,120p' server/hooks/bin/chat.ts
sed -n '1,120p' server/workers/chat/bin.ts
printf '%s\n' '--- directly bound symbols ---'
rg -n --glob '*.{ts,tsx,js,jsx}' 'function own|const own|export .*own|function secret|const secret|export .*secret|function connect|const connect|export .*connect|function parse|export .*parse' server

Repository: exactly/exa

Length of output: 49191


🏁 Script executed:

printf '%s\n' '--- supervision and ownership contracts ---'
sed -n '1,180p' server/supervise.ts
sed -n '1,140p' server/utils/secret.ts
sed -n '1,130p' server/workers/worker.ts
printf '%s\n' '--- analogous startup flows ---'
sed -n '1,100p' server/workers/hook/bin.ts
sed -n '1,100p' server/workers/refund/bin.ts
rg -n --glob 'server/**/*.ts' 'Promise\.all\(|\.finally\(|secrets\.close\(|bullmq\.quit\(' server

Repository: exactly/exa

Length of output: 19009


Close clients on partial startup failure.

If a later secret() call rejects after connect() resolves, own() never registers bullmq.quit() or secrets.close(). Add rejection cleanup in both startup files. Add a test that asserts each client closes once.

📍 Affects 2 files
  • server/hooks/bin/chat.ts#L14-L25 (this comment)
  • server/workers/chat/bin.ts#L16-L27

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d314f1472b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread infra/utils/modules.ts
crema: ["redis-address", "redis-password", "redis-username"],
services: {
chat: {
env: { WHATSAPP_PHONE_NUMBER_ID: "whatsappPhoneNumberId" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Configure the WhatsApp ID for every deployed stack

Adding this environment mapping makes infra/index.ts call config.require("whatsappPhoneNumberId") while constructing both chat modules for every stack. Only the sandbox and base-sepolia stack files define that key; the checked infra/Pulumi.base.yaml and infra/Pulumi.production.yaml files do not, so their deployment workflows now fail during pulumi up before any resources can be updated. Add the configuration to those stacks or omit the chat modules where they are not intended to run.

Useful? React with 👍 / 👎.

Comment thread server/hooks/chat.ts
text: [sender, ...tail].map(({ text }) => text).join("\n"),
})
.catch((error: unknown) => {
captureException(error, { extra: { sender }, tags: { job: "chat", queue: "chat" } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Redact user content from queue errors

When Redis rejects an enqueue, this sends the complete sender object to Sentry, including the customer's WhatsApp identifier, profile name, and free-form message text. During a queue outage every attempted support message can therefore be copied into the error-monitoring system and retained outside the chat path; users may include credentials or financial details in those messages. Report only non-content identifiers needed to diagnose the queue failure.

Useful? React with 👍 / 👎.

criteria: rubric.map((description) => ({ description })),
}).run({ input, output: text });
const executions = Object.values(judged.judge ?? {}).flatMap((step) => step.executions);
const agentUsage = { input: totalUsage.inputTokens ?? 0, output: totalUsage.outputTokens ?? 0 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include translator calls in evaluation usage totals

For every non-English evaluation case, the tool independently calls translator.generate once for the welcome script or twice for the help script, but those results are reduced to their text fields and their usage is discarded. Consequently this totalUsage accounts only for the outer chat-agent call, so the printed agent-token totals materially undercount the multilingual cases and cannot be used to compare their actual model consumption. Accumulate the translator results' usage alongside the outer result.

Useful? React with 👍 / 👎.

"You also get access to a dollar account in the US, all 100% free.",
"Create your account and activate your card here:",
].join("\n"),
url: appOrigin,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass the production app domain to chat workers

In the production deployment, server-production.yaml identifies the app domain as web.exactly.app, but the Pulumi stack contains no exa:domain, so infra/index.ts gives this worker the fallback APP_DOMAIN=production.exactly.app. Since appOrigin is embedded here in every welcome reply, production users receive an account-creation URL for the wrong host; the support script has the same problem. Persist or pass the workflow's production domain into the Pulumi deployment.

Useful? React with 👍 / 👎.

},
{
text: "If you don't find the answer there, write to us from the support chat inside the Exa app:",
url: `${appOrigin}/?support`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wire the support query to the Intercom presenter

For returning users this advertises /?support as the way to open the in-app support chat, but a repo-wide search of the route parameter consumers and the Intercom helpers finds no code that reads a support query parameter or calls present() in response to it. Clicking the link therefore only opens the normal root route and never presents the promised chat. Add a root/deep-link handler for this parameter or link to a route that already opens support.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d76b0ff3a0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +60 to +64
const seen = await bullmq.exists(`whatsapp:seen:${data.from}`);
const { text } = await reply(data.text, {
requestContext: new RequestContext<InferPublicSchema<typeof context>>([["seen", seen === 1]]),
});
await whatsapp.send(data.from, text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve per-sender ordering across delayed retries

When an earlier job for a sender fails transiently, BullMQ delays its retry and can process a later job from that sender first. The later job then observes seen as false, sends the welcome response, and records the sender; when the older job retries, it observes seen as true and sends the help response afterward, reversing the conversation and changing the older job's response between attempts. Serialize jobs by sender or otherwise prevent a delayed predecessor from being overtaken.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c8f477c42

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/instrument.cjs
const development = stack === "localhost";

init({
/** @type {import("@sentry/node").NodeOptions} */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove the prohibited JSDoc annotation

This newly added type comment is JSDoc, which the repository-wide comment convention explicitly prohibits; only suppression annotations, cspell:ignore, and TODO-style markers are permitted. Restructure the configuration so its type is inferred or checked without adding JSDoc to this entrypoint.

AGENTS.md reference: AGENTS.md:L94-L98

Useful? React with 👍 / 👎.

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.

2 participants