Skip to content
Merged
2 changes: 1 addition & 1 deletion examples/demo/src/app/token-staleness/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function TokenStalenessPage() {
const [newDisplayName, setNewDisplayName] = useState('');

// Get partial user from token (can be stale compared to actual user data)
const partialUserFromToken = app.usePartialUser({ from: 'token', or: 'anonymous' });
const partialUserFromToken = app.usePartialUser({ from: 'token', or: 'anonymous-if-exists' });

// Get raw tokens
const tokens = user?.currentSession.useTokens();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,17 +552,22 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
}

this._analyticsOptions = resolvedOptions.analytics;
const getAnalyticsAccessToken = async (): Promise<string | null> => {

const getAnalyticsSession = async (): Promise<InternalSession> => {
this._ensurePersistentTokenStore();
return await (await this.getUser({ or: "anonymous" })).getAccessToken();
const partialUser = await this.getPartialUser({ from: 'token', or: 'anonymous-if-exists' });
if (partialUser) {
return await this._getSession();
}
const anonUser = await this.getUser({ or: "anonymous" });
return anonUser._internalSession;
};
Comment thread
BilalG1 marked this conversation as resolved.

if (isBrowserLike() && this._analyticsOptions?.replays?.enabled === true) {
this._sessionRecorder = new SessionRecorder({
projectId: this.projectId,
getAccessToken: getAnalyticsAccessToken,
sendBatch: async (body, opts) => {
return await this._interface.sendSessionReplayBatch(body, await this._getSession(), opts);
return await this._interface.sendSessionReplayBatch(body, await getAnalyticsSession(), opts);
},
}, this._analyticsOptions.replays);
this._sessionRecorder.start();
Expand All @@ -571,9 +576,8 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
if (isBrowserLike()) {
this._eventTracker = new EventTracker({
projectId: this.projectId,
getAccessToken: getAnalyticsAccessToken,
sendBatch: async (body, opts) => {
return await this._interface.sendAnalyticsEventBatch(body, await this._getSession(), opts);
return await this._interface.sendAnalyticsEventBatch(body, await getAnalyticsSession(), opts);
},
});
this._eventTracker.start();
Expand Down Expand Up @@ -2686,7 +2690,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
return null;
}
const isAnonymous = accessToken.payload.is_anonymous;
if (isAnonymous && options.or !== "anonymous") {
if (isAnonymous && options.or !== "anonymous-if-exists") {
return null;
}
Comment thread
BilalG1 marked this conversation as resolved.
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { Result } from "@stackframe/stack-shared/dist/utils/results";
import { afterEach, describe, expect, it, vi } from "vitest";
import { EventTracker } from "./event-tracker";

async function advancePastAccessTokenRefresh() {
await vi.advanceTimersByTimeAsync(10_000);
await Promise.resolve();
async function advancePastFlush() {
await vi.advanceTimersByTimeAsync(10_000);
await Promise.resolve();
}
Expand Down Expand Up @@ -41,7 +39,6 @@ describe("EventTracker", () => {
const sentBodies: string[] = [];
const tracker = new EventTracker({
projectId: "internal",
getAccessToken: async () => "access-token",
sendBatch: async (body) => {
sentBodies.push(body);
return Result.ok(new Response());
Expand All @@ -56,7 +53,7 @@ describe("EventTracker", () => {
clientY: 34,
}));

await advancePastAccessTokenRefresh();
await advancePastFlush();

expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
[
Expand All @@ -79,7 +76,6 @@ describe("EventTracker", () => {
const sentBodies: string[] = [];
const tracker = new EventTracker({
projectId: "internal",
getAccessToken: async () => "access-token",
sendBatch: async (body) => {
sentBodies.push(body);
return Result.ok(new Response());
Expand All @@ -90,7 +86,7 @@ describe("EventTracker", () => {
tracker.start();
window.history.pushState({}, "", "/projects/test-project");

await advancePastAccessTokenRefresh();
await advancePastFlush();

expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ function hasHistoryMethods(value: unknown): value is { pushState: History["pushS

export type EventTrackerDeps = {
projectId: string,
getAccessToken: () => Promise<string | null>,
sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,
};

Expand All @@ -46,7 +45,6 @@ export class EventTracker {
private _flushTimer: ReturnType<typeof setInterval> | null = null;
private _events: TrackedEvent[] = [];
private _approxBytes = 0;
private _lastKnownAccessToken: string | null = null;
private _lastUrl: string | null = null;
private readonly _sessionReplaySegmentId: string;
private readonly _deps: EventTrackerDeps;
Expand Down Expand Up @@ -86,7 +84,7 @@ export class EventTracker {
clearInterval(this._flushTimer);
this._flushTimer = null;
}
runAsynchronously(() => this._flush({ keepalive: true }), { noErrorLogging: true });
runAsynchronously(() => this._flush({ keepalive: true }));
this._teardown();
}

Expand All @@ -99,7 +97,7 @@ export class EventTracker {
this._events.push(event);
this._approxBytes += JSON.stringify(event).length;
if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {
runAsynchronously(() => this._flush({ keepalive: false }), { noErrorLogging: true });
runAsynchronously(() => this._flush({ keepalive: false }));
}
}

Expand Down Expand Up @@ -226,7 +224,7 @@ export class EventTracker {
}

private readonly _onPageHide = () => {
runAsynchronously(() => this._flush({ keepalive: true }), { noErrorLogging: true });
runAsynchronously(() => this._flush({ keepalive: true }));
};

private _setupPageHideListeners() {
Expand Down Expand Up @@ -265,7 +263,6 @@ export class EventTracker {
}

private async _flush(options: { keepalive: boolean }) {
if (!this._lastKnownAccessToken) return;
if (this._events.length === 0) return;

const nowMs = Date.now();
Expand Down Expand Up @@ -298,14 +295,8 @@ export class EventTracker {

private _tick() {
if (this._cancelled) return;

runAsynchronously(async () => {
this._lastKnownAccessToken = await this._deps.getAccessToken();
}, { noErrorLogging: true });

const hasAuth = !!this._lastKnownAccessToken;
if (hasAuth && this._events.length > 0) {
runAsynchronously(() => this._flush({ keepalive: false }), { noErrorLogging: true });
if (this._events.length > 0) {
runAsynchronously(() => this._flush({ keepalive: false }));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export function getOrRotateSession(options: { key: string, nowMs: number }): Sto

export type SessionRecorderDeps = {
projectId: string,
getAccessToken: () => Promise<string | null>,
sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,
};

Expand All @@ -148,7 +147,6 @@ export class SessionRecorder {
private _lastBrowserSessionId: string | null = null;
private _takingSnapshot = false;
private _flushInProgress = false;
private _lastKnownAccessToken: string | null = null;
private readonly _sessionReplaySegmentId: string;
private readonly _storageKey: string;
private readonly _deps: SessionRecorderDeps;
Expand All @@ -172,7 +170,7 @@ export class SessionRecorder {
// Kick off rrweb recording
runAsynchronously(() => this._startRecording(), { noErrorLogging: true });

// Periodic flush + token refresh
// Periodic flush
this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);
}

Expand All @@ -183,7 +181,7 @@ export class SessionRecorder {
this._flushTimer = null;
}
// Flush remaining events before cleanup
runAsynchronously(() => this._flush({ keepalive: true }), { noErrorLogging: true });
runAsynchronously(() => this._flush({ keepalive: true }));
this._stopCurrentRecording();
}

Expand All @@ -202,7 +200,6 @@ export class SessionRecorder {
}

private async _flush(options: { keepalive: boolean }) {
if (!this._lastKnownAccessToken) return;
if (this._events.length === 0) return;
// Prevent concurrent in-flight HTTP requests. When a flush is already
// in-flight, a second batch could race on the server (both call
Expand Down Expand Up @@ -287,7 +284,7 @@ export class SessionRecorder {
this._events.push(event);
this._approxBytes += JSON.stringify(event).length;
if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {
runAsynchronously(() => this._flush({ keepalive: false }), { noErrorLogging: true });
runAsynchronously(() => this._flush({ keepalive: false }));
}
},
maskAllInputs: this._replayOptions.maskAllInputs ?? true,
Expand All @@ -298,7 +295,7 @@ export class SessionRecorder {
this._recording = true;

const onPageHide = () => {
runAsynchronously(() => this._flush({ keepalive: true }), { noErrorLogging: true });
runAsynchronously(() => this._flush({ keepalive: true }));
};
window.addEventListener("pagehide", onPageHide);
document.addEventListener("visibilitychange", onPageHide);
Expand All @@ -324,15 +321,8 @@ export class SessionRecorder {

private _tick() {
if (this._cancelled) return;

// Refresh the cached access token (async, fire-and-forget for this tick)
runAsynchronously(async () => {
this._lastKnownAccessToken = await this._deps.getAccessToken();
}, { noErrorLogging: true });

const hasAuth = !!this._lastKnownAccessToken;
if (hasAuth && this._events.length > 0) {
runAsynchronously(() => this._flush({ keepalive: false }), { noErrorLogging: true });
if (this._events.length > 0) {
runAsynchronously(() => this._flush({ keepalive: false }));
Comment thread
BilalG1 marked this conversation as resolved.
}
}
}
2 changes: 1 addition & 1 deletion packages/template/src/lib/stack-app/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export type ConvexCtx =

export type GetCurrentPartialUserOptions<HasTokenStore> =
& {
or?: 'return-null' | 'anonymous', // note: unlike normal getUser, 'anonymous' still returns null sometimes (eg. if no token is present)
or?: 'return-null' | 'anonymous-if-exists', // note: unlike normal getUser, 'anonymous' still returns null sometimes (eg. if no token is present)
tokenStore?: TokenStoreInit,
Comment thread
BilalG1 marked this conversation as resolved.
}
& (
Expand Down
Loading