mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +08:00
Merge branch 'dev' into promptless/document-developer-tools
This commit is contained in:
commit
62c1dc330f
@ -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();
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
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();
|
||||
@ -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();
|
||||
@ -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;
|
||||
}
|
||||
return {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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());
|
||||
@ -56,7 +53,7 @@ describe("EventTracker", () => {
|
||||
clientY: 34,
|
||||
}));
|
||||
|
||||
await advancePastAccessTokenRefresh();
|
||||
await advancePastFlush();
|
||||
|
||||
expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
||||
[
|
||||
@ -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());
|
||||
@ -90,7 +86,7 @@ describe("EventTracker", () => {
|
||||
tracker.start();
|
||||
window.history.pushState({}, "", "/projects/test-project");
|
||||
|
||||
await advancePastAccessTokenRefresh();
|
||||
await advancePastFlush();
|
||||
|
||||
expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
||||
[
|
||||
|
||||
@ -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>>,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +224,7 @@ export class EventTracker {
|
||||
}
|
||||
|
||||
private readonly _onPageHide = () => {
|
||||
runAsynchronously(() => this._flush({ keepalive: true }), { noErrorLogging: true });
|
||||
runAsynchronously(() => this._flush({ keepalive: true }));
|
||||
};
|
||||
|
||||
private _setupPageHideListeners() {
|
||||
@ -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();
|
||||
@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>>,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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,
|
||||
@ -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);
|
||||
@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
& (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user