Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,5 @@ export type {
RemoveAccessTokenOptions,
GetLoginUrlOptions,
} from "./utils/auth-utils.types.js";

export { getAnonymousVisitorId } from "./modules/analytics.js";
24 changes: 24 additions & 0 deletions packages/sdk/src/modules/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,3 +452,27 @@ export function getAnalyticsSessionId(): string {
return getFallbackSessionId();
}
}

/**
* Gets the anonymous visitor id the SDK already sends as `X-Base44-Anonymous-Id` on requests made
* without a signed-in user.
*
* Use it to send the same id on requests the SDK doesn't make itself, such as calls to an app's own
* server functions. Outside a browser it returns `null`.
*
* @returns The visitor id, or `null` outside a browser.
*
* @example
* ```typescript
* const visitorId = getAnonymousVisitorId();
* const headers = visitorId ? { "X-Base44-Anonymous-Id": visitorId } : {};
* ```
*/
export function getAnonymousVisitorId(): string | null {
try {
if (typeof window === "undefined") return null;
return getAnalyticsSessionId();
} catch {
return null;
}
}
42 changes: 42 additions & 0 deletions packages/sdk/tests/unit/anonymous-visitor-header.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { createAxiosClient } from "../../src/utils/axios-client.ts";
import { getAnalyticsSessionId } from "../../src/modules/analytics.ts";
import { getAnonymousVisitorId } from "../../src/index.ts";

const ANALYTICS_SESSION_ID_KEY = "base44_analytics_session_id";

Expand Down Expand Up @@ -113,3 +114,44 @@ describe("anonymous visitor header", () => {
});
});
});

// The TanStack template sends this id itself on server-function calls, which use TanStack's fetch
// rather than the SDK's client.
describe("getAnonymousVisitorId", () => {
test("is the id an unauthenticated client sends as X-Base44-Anonymous-Id", async () => {
const headers = await captureRequestHeaders(undefined);
expect(getAnonymousVisitorId()).toBe(headers.get("X-Base44-Anonymous-Id"));
});

test("is null outside a browser", () => {
vi.stubGlobal("window", undefined);
expect(getAnonymousVisitorId()).toBeNull();
});

test("is the analytics session id, and persists it", () => {
const id = getAnonymousVisitorId();
expect(id).toBeTruthy();
expect(id).toBe(getAnalyticsSessionId());
expect(
(globalThis as any).localStorage.getItem(ANALYTICS_SESSION_ID_KEY)
).toBe(id);
expect(getAnonymousVisitorId()).toBe(id);
});

test("reuses an id already stored", () => {
(globalThis as any).localStorage.setItem(
ANALYTICS_SESSION_ID_KEY,
"existing-id"
);
expect(getAnonymousVisitorId()).toBe("existing-id");
});

test("never throws when localStorage throws", () => {
vi.stubGlobal("localStorage", {
getItem() {
throw new Error("blocked");
},
});
expect(() => getAnonymousVisitorId()).not.toThrow();
});
});