50949 frontend [ sentry ] handle expired or missing device token on reset by Maria-Lordwill · Pull Request #403 · pneumaticapp/pneumaticworkflow · GitHub
Skip to content

50949 frontend [ sentry ] handle expired or missing device token on reset - #403

Open
Maria-Lordwill wants to merge 2 commits into
masterfrom
frontend/sentry/50949__handle_expired_or_missing_device_token_on_rese
Open

Maria-Lordwill wants to merge 2 commits into
masterfrom
frontend/sentry/50949__handle_expired_or_missing_device_token_on_rese

Conversation

@Maria-Lordwill

@Maria-Lordwill Maria-Lordwill commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

1. Release notes

Fixed an issue during logout or tenant switching where the device push token was previously deleted or expired on the server. The logout flow and workspace switching now complete cleanly without background errors.

2. Description (problem)

When logging out (Logout) or switching tenant (Switch Tenant, /tenants/ page), the client sends a DELETE /notifications/device/<token> request to detach the browser push token from the current user.

If the token was already automatically deleted on the backend (for example, when Google Firebase returned UnregisteredError or InvalidArgumentError during push delivery, prompting the server to delete the device record without notifying the client), or if a token from another user session remained in localStorage, the backend returns standard DRF 404 Not Found ({"detail":"Not found."}).

Previously, the global Axios interceptor (commonRequest) unconditionally logged any non-2xx response as an error (logger.error), triggering false positive issues in Sentry.

Where it occurred: on the /tenants/ page when switching organizations, and during user logout (Logout).

3. Context

  • Task ID: 50949
  • Sentry Issue: PNEUMATIC-FRONTEND-PROD-24V
  • Sentry Project: pneumatic-frontend-prod
  • Event count: 2 | Impacted users: 2
  • Timeframe: First Seen: 2026-10-15 14:08:15 UTC → Last Seen: 2026-10-15 14:08:51 UTC
  • Environment: prod
  • URL: https://my.pneumatic.app/tenants/
  • Code locations: frontend/src/public/api/commonRequest.ts, frontend/src/public/api/resetFirebaseDeviceToken.ts, frontend/src/public/firebase/utils.ts, frontend/src/public/redux/auth/saga.ts.

4. Solution

  • Added support for declaring expected error HTTP status codes (expectedErrorStatuses) in commonRequest.
  • When an API response returns an HTTP status included in expectedErrorStatuses, the interceptor logs it as an informational message (logger.info), preventing false error events in Sentry. The HTTP status is not masked as 200 OK — transport contract integrity is preserved.
  • Declared status 404 as expected for device token reset (expectedErrorStatuses: [404]) in resetFirebaseDeviceToken.
  • Made resetFirebaseDeviceToken in firebase/utils.ts asynchronous: the local token is now guaranteed to be removed from localStorage (localStorage.removeItem(FIREBASE_DEVICE_STORAGE_KEY)) after the API call, and the logout saga now waits for it via yield call(resetFirebaseDeviceToken).

5. Implementation details

  • Request error interceptor (commonRequest.ts):
    • Was: Any non-success error not matching isExpectedClientError(responseData) was logged with logger.error, generating a Sentry event.
    • Now: Added expectedErrorStatuses?: number[] to ICommonRequestOptions; the matching AxiosRequestConfig augmentation lives in typings/axios.d.ts (see item 7). When the response status matches an entry in the array, the error is logged as logger.info.
  • Token reset API (resetFirebaseDeviceToken.ts):
    • Was: Request was called with { shouldThrow: false, responseType: 'empty' }.
    • Now: Added expectedErrorStatuses: [404].
  • Local token cleanup (firebase/utils.ts):
    • Was: resetFirebaseDeviceTokenAPI was called synchronously without awaiting response, and localStorage was never cleared.
    • Now: Function is declared async: it awaits await resetFirebaseDeviceTokenAPI(token) and then executes localStorage.removeItem(FIREBASE_DEVICE_STORAGE_KEY).
  • Logout saga (redux/auth/saga.ts):
    • Was: Direct synchronous call to resetFirebaseDeviceToken().
    • Now: Delegated to saga effect: yield call(resetFirebaseDeviceToken).

6. What to test

6.1 Preconditions

  • To verify Sentry event suppression (item 6.4), the staging environment is required (Sentry is disabled in local).
  • A test account with ability to log out and switch organizations (/tenants/).
  • Browsers: Chrome Desktop, Safari Desktop, Chrome Mobile, Safari Mobile.

6.2 Positive scenarios

Scenario Description (what to do and what to expect) Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
Normal token reset on Logout (204 No Content) Log in to an account and verify firebase_device_token exists in localStorage. Click Logout. Expected: a DELETE /notifications/device/<token> request is sent in Network and returns 204. Token is removed from localStorage. User is redirected to login page. [ ] [ ] [ ] [ ]
Token reset on tenant switch (/tenants/) Log in with a user belonging to two or more organizations. Navigate to /tenants/ and switch to another organization. Expected: tenant switch completes successfully and redirects without UI errors. [ ] [ ] [ ] [ ]

6.3 Negative scenarios and edge cases

Scenario Description (what to do and what to expect) Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
Reset of already deleted server token (404 Not Found) In browser DevTools manually set firebase_device_token in localStorage to a fake value deleted_token_404. Click Logout. Expected: DELETE /notifications/device/deleted_token_404 returns HTTP 404 {"detail":"Not found."}. The response is logged to console as info (no red error). No Sentry event is created on staging. Token is removed from localStorage. Logout completes successfully. [ ] [ ] [ ] [ ]
Network error 500 during token reset Mock a 500 response on DELETE /notifications/device/<token>. Expected: HTTP 500 is not in expectedErrorStatuses, so it is logged via logger.error and sent to Sentry. Due to shouldThrow: false, the user logout flow is not interrupted. [ ] [ ] [ ] [ ]
404 on other endpoints still reported to Sentry Make a request to any other application endpoint returning 404 (without expectedErrorStatuses). Expected: error is logged via logger.error and sent to Sentry as usual. [ ] [ ] [ ] [ ]
Logout without token in localStorage Clear the firebase_device_token key in localStorage and click Logout. Expected: no token deletion request is sent, logout succeeds normally without errors. [ ] [ ] [ ] [ ]

6.4 Verification points

  • Sentry (staging): on receiving 404 from /notifications/device/<token>, no new Response Error: 404 {"detail":"Not found."} events are created.
  • Browser console: 404 response on device token reset is printed at info level, not error.
  • Network (DevTools): DELETE /notifications/device/<token> request is made and retains its 404 status (backend HTTP contract is not masked).
  • Application → Local Storage: firebase_device_token key is removed from localStorage under both outcomes (204 success or 404 not found).

6.5 API checks

  • Outgoing request:
    • Method: DELETE
    • URL: /api/v2/notifications/device/<token>/ (where <token> is the value from localStorage.firebase_device_token).
    • Request body: empty.
  • Incoming response:
    • When token exists: HTTP 204 No Content.
    • When token is missing or expired: HTTP 404 Not Found, body {"detail":"Not found."}.
    • Client behavior: in both cases the logout or tenant switch flow continues uninterrupted.

6.6 What was NOT tested

  • Real push notification delivery via Google Firebase Cloud Messaging (only device token deregistration was tested).
  • Manual testing in all 4 browser environments (tables in 6.2, 6.3, 7, and 8 are provided as a blank QA checklist).
  • Load testing.
  • Production environment verification (verified via unit test suite and local builds).

7. Refactoring

The refactoring keeps type declarations in one place of the project (src/typings) instead of scattering them across application code.

Axios type declaration for expectedErrorStatuses.

  • Description: The AxiosRequestConfig augmentation with the expectedErrorStatuses field was declared directly in commonRequest.ts (declare module 'axios'). It was moved to a separate file, frontend/src/typings/axios.d.ts, next to the other type declaration files of the project. commonRequest.ts now has an import type {} from '../../typings/axios', which makes the augmentation visible to ts-node (it ignores include from tsconfig, as noted in the code comment). Application behavior does not change — only the type declaration was moved.
  • What to check:
Scenario Description (what to do and what to expect) Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
Client build In the frontend directory run npm run build-client:prod. Expected: the build finishes without TypeScript and webpack errors. [ ] [ ] [ ] [ ]
Dev server start (ts-node) In the frontend directory run npm run local. Expected: the server starts without TypeScript errors and the application page opens. [ ] [ ] [ ] [ ]
expectedErrorStatuses still works Repeat the scenario "Reset of already deleted server token (404 Not Found)" from 6.3. Expected: behavior is unchanged — 404 is logged as info, no Sentry event is created. [ ] [ ] [ ] [ ]

8. Testing of affected places

Changes touched the global commonRequest.ts interceptor used by all API requests in the application, and the logout saga in redux/auth/saga.ts.

Scenario Description (what to do and what to expect) Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
Core application API requests Log in to the application and navigate main pages (dashboard, workflows, tasks). Expected: all standard API requests work properly, successful responses and standard errors are handled without alterations. [ ] [ ] [ ] [ ]
Standard API error logging Trigger a standard API error (e.g. invalid credentials or 404 on an unknown resource). Expected: the interceptor retains default logging behavior (logger.error call and dispatch to Sentry). [ ] [ ] [ ] [ ]

9. Unit tests

The following scenarios are covered:

  1. interceptorError.test.ts:
    • Request with expectedErrorStatuses: [404] upon receiving 404 invokes logger.info without calling logger.error.
    • Request without expectedErrorStatuses upon receiving 404 continues calling logger.error.
  2. resetFirebaseDeviceToken.test.ts:
    • Invocation of resetFirebaseDeviceToken(token) sends HTTP method DELETE, correct URL /notifications/device/<token>, and options { shouldThrow: false, responseType: 'empty', expectedErrorStatuses: [404] }.
  3. utils.test.ts:
    • Function resetFirebaseDeviceToken() reads token from localStorage, calls reset API, and removes token from localStorage.
    • When no token is in localStorage, no API request is sent.

All tests were validated using TDD (failed with Expected logger.info / Received logger.error on unpatched code, pass cleanly on patched code).

10. Commits

  • 5c9699071 — 50949 fix(api): ignore 404 on device token reset
  • 1c09b2fca — 50949 refactor(api): move axios type to typings

Closes https://pneumatic.sentry.io/issues/PNEUMATIC-FRONTEND-PROD-24V


Note

Medium Risk
Touches the global Axios response interceptor used by all API calls, though behavior only changes when callers opt in via expectedErrorStatuses; logout timing now waits on token reset.

Overview
Fixes false Sentry noise when DELETE device token returns 404 during logout or tenant switch.

commonRequest gains optional expectedErrorStatuses: matching HTTP statuses are logged with logger.info instead of logger.error (responses still reject as before). resetFirebaseDeviceToken passes [404] for already-missing tokens.

Firebase reset flow is now async: it awaits the DELETE, then localStorage.removeItem for the device token key (replacing clearing to an empty string). The logout saga yield call(resetFirebaseDeviceToken) so cleanup finishes before the rest of sign-out.

Unit tests cover interceptor logging, the API client options, and Firebase util behavior.

Reviewed by Cursor Bugbot for commit 1c09b2f. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

[!NOTE]

Treat expired or missing device token on logout as expected, not an error

  • Makes firebase.resetFirebaseDeviceToken in utils.ts async: it awaits the DELETE API call before updating localStorage, and removes the Firebase token key instead of storing an empty string. The logout saga in saga.ts now yields this call.
  • Adds an optional expectedErrorStatuses array to AxiosRequestConfig and ICommonRequestOptions in commonRequest.ts. The response interceptor logs matching statuses with logger.info instead of logger.error; rejection and API error creation are unchanged.
  • The device-token DELETE client in resetFirebaseDeviceToken.ts passes expectedErrorStatuses: [404], so a missing token on logout is no longer logged as an error. Adds test coverage for the interceptor, API clients, and Firebase utilities.
  • Behavioral Change: firebase.resetFirebaseDeviceToken is now async and callers that don't await it will see timing differences; logout waits for the API call before clearing superuser tokens and analytics.

Changes since #403 opened

  • Moved AxiosRequestConfig type augmentation to dedicated ambient declaration file [1c09b2f]

Macroscope summarized 5c96990.

@Maria-Lordwill Maria-Lordwill added bug Something isn't working Frontend Web client changes request Sentry Issues in Sentry labels Oct 16, 2026
Comment thread frontend/src/public/api/commonRequest.ts Outdated

This branch has not been deployed

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

Labels

bug Something isn't working Frontend Web client changes request Sentry Issues in Sentry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants