Skip to content

fix(api): allow creating and editing Sources in Local App Mode#2602

Open
brandon-pereira wants to merge 1 commit into
mainfrom
brandon/fix-sources-local-app-mode-a34b
Open

fix(api): allow creating and editing Sources in Local App Mode#2602
brandon-pereira wants to merge 1 commit into
mainfrom
brandon/fix-sources-local-app-mode-a34b

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Since @hyperdx/app@v2.22.0, creating or editing a Source from Team Settings → Data → Sources fails with HTTP 500 "Failed to update source" when running HyperDX in Local App Mode (IS_LOCAL_APP_MODE="DANGEROUSLY_is_local_app_mode💀").

Root cause: In Local App Mode, the auth middleware (isUserAuthenticated) injects a plain string team id ("_local_team_") onto req.user.team instead of a Mongoose ObjectId. The POST /sources and PUT /sources/:id handlers built the request body with team: teamId.toJSON(). .toJSON() only exists on ObjectId, not on strings, so the handler threw:

TypeError: teamId.toJSON is not a function
    at .../routers/api/sources.js:49

GET /sources was unaffected because it already used teamId.toString(), which is why listing sources worked but saving them did not.

Fix: Use teamId.toString() in both the create and update handlers. It returns the correct value for both a string team id (Local App Mode) and an ObjectId (normal auth). The team value in the body is also overridden downstream by the controllers, so this is safe.

How to test on Vercel preview

N/A — non-UI change (API-only bug fix).

Testing

Added two regression tests in packages/api/src/routers/api/__tests__/sources.test.ts that mount the sources router with a middleware emulating the Local App Mode string team id and exercise POST / and PUT /:id.

  • With the pre-fix teamId.toJSON() code, both tests fail with the exact error from the issue (TypeError: teamId.toJSON is not a function).
  • With the fix, both tests pass, and the full sources integration suite (34 tests) passes.

References

Linear Issue: HDX-4713

Open in Web Open in Cursor 

In Local App Mode the auth middleware injects a plain string team id onto
req.user instead of a Mongoose ObjectId. The sources POST/PUT handlers called
teamId.toJSON(), which only exists on ObjectId, throwing
'TypeError: teamId.toJSON is not a function' and returning HTTP 500 when
saving a Source. Use teamId.toString(), which works for both string and
ObjectId team ids, and add regression tests covering the string-team-id path.

Fixes HDX-4713

Co-authored-by: Brandon Pereira <brandon-pereira@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fda1316

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

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector 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

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 7, 2026 5:02pm
hyperdx-storybook Ready Ready Preview, Comment Jul 7, 2026 5:02pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes an HTTP 500 crash in Local App Mode when saving a Source by replacing teamId.toJSON() (an ObjectId-only method) with teamId.toString() (works for both string and ObjectId) in the POST /sources and PUT /sources/:id handlers. The GET /sources handler already used .toString(), so only the create/update paths were broken.

  • packages/api/src/routers/api/sources.ts: Two one-line changes — teamId.toJSON()teamId.toString() in the POST and PUT handlers. Both calls become consistent with the existing GET handler and with how teamId is passed to the controller functions on the same lines.
  • packages/api/src/routers/api/__tests__/sources.test.ts: Two new regression tests that mount the sources router behind a middleware emulating the Local App Mode string team id and exercise the broken POST / and PUT /:id paths end-to-end.
  • .changeset/fix-sources-local-app-mode.md: Patch changeset entry for @hyperdx/api.

Confidence Score: 5/5

Safe to merge — the change is minimal, targeted, and fully backward-compatible.

Both changed lines are mechanically equivalent for the normal ObjectId path (.toString() produces the same hex string as .toJSON() on an ObjectId) and correctly handle the Local App Mode string path that was broken. The fix is consistent with how the GET handler and controller call-sites already used .toString(). Two focused regression tests exercise the previously crashing code paths end-to-end.

No files require special attention.

Important Files Changed

Filename Overview
packages/api/src/routers/api/sources.ts Two-line fix: replaces teamId.toJSON() with teamId.toString() in POST and PUT handlers, making them consistent with GET and safe for both ObjectId and string team ids.
packages/api/src/routers/api/tests/sources.test.ts Adds two well-scoped regression tests for Local App Mode (string team id) covering both POST and PUT endpoints; test setup correctly mirrors production middleware ordering.
.changeset/fix-sources-local-app-mode.md Correct patch-level changeset entry for @hyperdx/api with an accurate description of the bug and fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Middleware as isUserAuthenticated<br/>(Local App Mode)
    participant Router as sources.ts router
    participant Controller as createSource / updateSource

    Client->>Middleware: POST /sources or PUT /sources/:id
    Middleware->>Router: "req.user.team = "_local_team_" (string)"

    Note over Router: teamId = req.user.team (string in Local Mode)

    alt Before fix (toJSON)
        Router->>Router: teamId.toJSON() 💥 TypeError
        Router-->>Client: HTTP 500 "Failed to update source"
    else After fix (toString)
        Router->>Controller: "createSource / updateSource(teamId.toString(), { team: teamId.toString(), ... })"
        Controller-->>Router: saved Source document
        Router-->>Client: HTTP 200 OK
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Middleware as isUserAuthenticated<br/>(Local App Mode)
    participant Router as sources.ts router
    participant Controller as createSource / updateSource

    Client->>Middleware: POST /sources or PUT /sources/:id
    Middleware->>Router: "req.user.team = "_local_team_" (string)"

    Note over Router: teamId = req.user.team (string in Local Mode)

    alt Before fix (toJSON)
        Router->>Router: teamId.toJSON() 💥 TypeError
        Router-->>Client: HTTP 500 "Failed to update source"
    else After fix (toString)
        Router->>Controller: "createSource / updateSource(teamId.toString(), { team: teamId.toString(), ... })"
        Controller-->>Router: saved Source document
        Router-->>Client: HTTP 200 OK
    end
Loading

Reviews (1): Last reviewed commit: "fix(api): use teamId.toString() in sourc..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 224 passed • 3 skipped • 1494s

Status Count
✅ Passed 224
❌ Failed 0
⚠️ Flaky 3
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@brandon-pereira brandon-pereira marked this pull request as ready for review July 7, 2026 17:19
@brandon-pereira brandon-pereira added the ai-generated AI-generated content; review carefully before merging. label Jul 7, 2026
@brandon-pereira brandon-pereira requested a review from knudtty July 7, 2026 17:19
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Touches API routes or data models — hidden complexity risk

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 1
  • Production lines changed: 4 (+ 71 in test files, excluded from tier calculation)
  • Branch: brandon/fix-sources-local-app-mode-a34b
  • Author: brandon-pereira

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

The core change (teamId.toJSON()teamId.toString() in the POST / and PUT /:id sources handlers) is minimal and behavior-preserving. For a Mongoose ObjectId, toJSON() and toString() return the same hex string, so normal multi-tenant auth is unchanged; for the Local App Mode string team id (_local_team_), toString() is idempotent while toString()-vs-toJSON() was the only crash site. The change also brings POST/PUT in line with GET/DELETE, which already used toString(). No tenant-isolation regression, no API contract change, and middleware/auth.ts is untouched.

🟡 P2 -- recommended

  • packages/api/src/routers/api/__tests__/sources.test.ts:733 -- the new local-app-mode PUT test only exercises the same-kind findOneAndReplace branch; the kind-change replaceOne branch of updateSource, now reachable in Local App Mode for the first time since the fix removes the router-level 500, has no coverage under a string team id.
    • Fix: add a PUT /:id test that changes the source kind under the string team id to exercise the raw replaceOne path.
    • testing, correctness
🔵 P3 nitpicks (3)
  • packages/api/src/routers/api/__tests__/sources.test.ts:701 -- getLocalAppModeApp() re-declares the /sources mount path and appErrorHandler wiring that api-app.ts owns, so the test app could silently diverge if production mounting gains additional middleware.
    • Fix: extract a shared test-app helper (e.g. in fixtures.ts) that mounts a router with a synthetic req.user so mounting changes live in one place.
    • testing, maintainability
  • packages/api/src/middleware/auth.ts:121 -- req.user.team is statically typed as Types.ObjectId but assigned a plain string via @ts-ignore in Local App Mode, so the type system cannot catch the next ObjectId-only call added to this auth flow; the fix patches the two known call sites rather than the type gap. Pre-existing.
    • Fix: type the team id as a Types.ObjectId | string union (e.g. from getNonNullUserWithTeam) so future callers are forced to normalize.
  • .changeset/fix-sources-local-app-mode.md -- the associated commit carries a Co-Authored-By trailer, which the repo's AGENTS.md Git Commits rule instructs against; this is git metadata, not code.
    • Fix: drop the trailer on future commits per the stated convention.

Reviewers (7): correctness, security, api-contract, testing, maintainability, project-standards, kieran-typescript.

Testing gaps:

  • Kind-change (replaceOne) path in updateSource is untested under a string team id, though code reading indicates it derives team from existing.team and should behave correctly.

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

Labels

ai-generated AI-generated content; review carefully before merging. review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP 500 "Failed to update source" when creating or editing Sources in Local App Mode

3 participants