Skip to content

fix: [SDK-5466] send on_session again when a token arrives after the session started under IV - #1527

Merged
sherwinski merged 6 commits into
feat/identity-verificationfrom
sherwin/sdk-5466
Oct 26, 2026
Merged

sherwinski merged 6 commits into
feat/identity-verificationfrom
sherwin/sdk-5466

Conversation

@sherwinski

@sherwinski sherwinski commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Description

1 Line Summary

Send the on_session update again when a JWT arrives after the session started under Identity Verification, on the page and in the worker.

Details

Under Identity Verification the on_session update (session_count: 1) for a page view was lost in 2 cases. Both match the loss on main today when the unsigned request gets a 401.

Page, no worker (SessionManager._sendOnSessionUpdateFromPage): a 401 set _onSessionSent = true, so the page did not try again after the app stored a fresh token with updateUserJwt or login.

  • The flag now stays clear on a 401 or a failed request. A 200 or a 403 sets it.
  • Sends run one at a time on a promise chain, so a token update during a request re-checks the flag after that request settles instead of a second session_count: 1.
  • internalInit registers a JwtTokenStore update listener that calls the new SessionManager._handleJwtUpdated(externalId).
  • _handleJwtUpdated does nothing before the session start (the normal flow carries the token), when IV is inactive, or when the token is for another user. Without a worker it calls _sendOnSessionUpdateFromPage again, which re-checks every guard. With a worker it asks for an upsert with the new token (SessionOrigin._JwtUpdated), but only from a visible, focused tab: an upsert from a hidden tab resets the worker's finalize timer, and the next focus upsert carries the token anyway.

Worker (upsertSession in helpers/service-worker.ts): a returning identified user with no stored token at session start marked the session Active and skipped the user request. A later updateUserJwt or login never sent it, because the next upsert hit Session already active. A 401 was lost the same way.

  • updateUserSession now returns false when it skipped the request, got a 401, or the request did not reach the server. Any other response counts as sent.
  • sendOnSessionCall stores Session.onSessionPendingFor = externalId when the request did not count as sent, the payload has jwtRequired, and it has an externalId. An anonymous user under IV has no backend user, so that skip does not leave the session pending. Without jwtRequired the legacy path is unchanged.
  • An upsert on an active session, or a reactivation within the threshold, sends the pending request once for the same externalId and clears the field. A different user clears it and sends nothing. The field is cleared before the request and set back on a skip or a 401, so an upsert that overlaps a slow request finds nothing pending. The request can retry for 120 s, past the session threshold, so the flag goes on the stored row read after the request, and only if it is still the same session. A write of the captured object could recreate a row finalizeSession deleted.

The // TO DO: aggregate session count in case network call fails stays as is. This PR adds no counter.

Open points, left for a follow-up

  • The anonymous user. An anonymous user under IV who logs in on the same page view gets no on_session for the new user, because the anonymous skip does not set the pending field. On main, the anonymous on_session goes out at session start and the identify call keeps the same onesignal_id, so that user's session is counted.
  • A login as a different user. _putJwt fires the token listener before the identity model switches, so _handleJwtUpdated compares against the old external id and returns. Nothing fires it again after the switch.
  • Replay of a rejected token. A pending session sends again on the next upsert with whatever token the page has. If the page still has the rejected token, the worker gets another 401. With feat: [SDK-5465] broadcast a worker 401 to the page so the page invalidates the JWT #1528 that 401 reaches the page, which removes the token and fires userJwtInvalidated, so the replay stops once the app refreshes the token.

Bundle size

Measured with size-limit on both builds:

Bundle Base (feat/identity-verification) This branch Delta Limit
OneSignalSDK.page.es6.js 45.21 kB 45.32 kB +110 B 45.34 kB
OneSignalSDK.sw.js 12.68 kB 12.82 kB +140 B 12.83 kB

The base already sat at both limits, so the build: commits raise them.

Systems Affected

  • WebSDK
  • Backend
  • Dashboard

Validation

Tests

Info

  • vp test run — 72 files, 871 tests pass.
  • vp check — 0 errors, 0 warnings in the changed files.
  • vp run build:prod — passes with the raised limits (45.32 kB page, 12.82 kB worker).

New tests:

  • SessionManager.test.ts: a 401 or a failed request does not count as sent and a fresh token sends again; a 403 counts as sent; a send that starts while one is in flight sends nothing more; _handleJwtUpdated without a worker sends the skipped on_session once; with a worker a visible focused tab posts an upsert with _JwtUpdated and the token, and a hidden or unfocused tab posts nothing; nothing before the session start, when IV is inactive, or for another user.
  • ServiceWorker.test.ts: a skipped request, a 401, or a failed request stores the session as pending for the user; a later upsert with a token sends it once and a further upsert sends nothing; a 400 counts as sent; a pending inactive session reactivated within the threshold sends it; the pending request is not sent to a different user; two overlapping upserts send it once; a session finalized while the request retries is not recreated; a pending restore after a failed retry goes on the current session only; an anonymous user does not leave the session pending.
  • init.test.ts: internalInit registers the listener, so a stored token reaches _handleJwtUpdated.

Checklist

  • All the automated tests pass or I explained why that is not possible
  • I have personally tested this on my machine or explained why that is not possible
  • I have included test coverage for these changes or explained why they are not needed

Programming Checklist
Interfaces:

  • Don't use default export
  • New interfaces are in model files

Functions:

  • Don't use default export
  • All function signatures have return types
  • Helpers should not access any data but rather be given the data to operate on.

Typescript:

  • No Typescript warnings
  • Avoid silencing null/undefined warnings with the exclamation point

Other:

  • Iteration: refrain from using elem of array syntax. Prefer forEach or use map
  • Avoid using global OneSignal accessor for context if possible. Instead, we can pass it to function/constructor so that we don't call OneSignal.context

Screenshots

Info

Not needed: no UI change.

Checklist

  • I have included screenshots/recordings of the intended results or explained why they are not needed

Related Tickets


@sherwinski
sherwinski added this pull request to stack #1533 October 20, 2026 23:40
@sherwinski sherwinski changed the title fix: [SDK-5466] send on_session again when a token arrives after the session started under IV fix: [SDK-5466] send on_session again when a token arrives after the session started under IV Oct 20, 2026

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The page path does what the PR says: a 401 leaves _onSessionSent clear, and a newer token is kept. Two holes in the retry.

  1. On the worker, a 401 counts as sent, so onSessionPending never sticks and a later token hits "session already active".
  2. On the page, _onSessionSent is set only after the request returns, so two token writes in that window both send session_count: 1.

Comment thread src/shared/api/sw.ts Outdated
Comment thread src/shared/managers/sessionManager/SessionManager.ts Outdated
Comment thread src/shared/helpers/service-worker.ts
Comment thread src/shared/managers/sessionManager/SessionManager.ts
Comment thread src/shared/managers/sessionManager/SessionManager.ts
Comment thread src/shared/managers/sessionManager/SessionManager.ts

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A failed on_session writes back the session object captured before the request. That put can recreate a row finalizeSession already deleted.

Comment thread src/shared/helpers/service-worker.ts Outdated
…rted under IV

Under Identity Verification the on_session update for a page view was
lost in 2 cases. Both match the loss on main when the unsigned request
gets a 401.

Page (no worker): a 401 set _onSessionSent, so the page never tried
again after the app stored a fresh token. The flag now stays clear on a
401, and a token stored for the current user after the session start
sends the update again through _handleJwtUpdated.

Worker: a returning identified user with no stored token at session
start marked the session Active and skipped the user request. A later
token never sent it, because the next upsert hit 'Session already
active'. The session now records onSessionPending when the request was
skipped for a missing token. The next upsert that brings a token sends
it once, on an active session and on a reactivation within the
threshold. An anonymous user under IV has no backend user and does not
leave the session pending.

The page registers a JwtTokenStore update listener in internalInit. With
a worker it asks for an upsert with the new token (SessionOrigin
_JwtUpdated); without a worker it calls _sendOnSessionUpdateFromPage
again.
…worker)

The on_session resend adds 75 B gzip to the page bundle and 67 B to the
worker. The base already sat at both limits.
…nd page view

Worker: updateUserSession counts only a response other than 401 as sent, so a
401 or a failed request leaves the session pending. The pending flag now stores
the external id, and the next upsert sends only for that user. The flag is
cleared before the request, so an upsert that overlaps a slow request does not
send a second on_session.

Page: on_session sends run one at a time, so a token update during a request
re-checks the sent flag after it. A 403 counts as sent. With a worker, a hidden
or unfocused tab does not post the token upsert, because that upsert resets the
worker's finalize timer.
The request can retry for 120 s, past the session threshold, so the row the
request started with may be deleted or replaced by the time it gives up. A
write of the captured object recreated a row finalizeSession deleted. The flag
now goes on the current row, and only if it is still the same session.
@sherwinski
sherwinski merged commit 051d79b into feat/identity-verification Oct 26, 2026
1 check passed
@sherwinski
sherwinski deleted the sherwin/sdk-5466 branch October 26, 2026 20:31
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.

3 participants