From c490c181c74ba89a607057d13cb297047509779b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 17 Feb 2026 17:10:18 -0600 Subject: [PATCH] Standardize headers --- src/analytics/identifiers/session.ts | 10 ++++++++++ src/analytics/identifiers/session.web.ts | 10 ++++++++++ src/analytics/utils.ts | 18 ++++++++++++++++++ src/lib/api/feed/custom.ts | 4 ++-- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/analytics/identifiers/session.ts b/src/analytics/identifiers/session.ts index e94ece704f..1699d474a2 100644 --- a/src/analytics/identifiers/session.ts +++ b/src/analytics/identifiers/session.ts @@ -18,6 +18,16 @@ export function getInitialSessionId() { return sessionId } +/** + * Gets the current session ID. Freshness depends on `useSessionId` being + * mounted, which handles refreshing this value between foreground/background + * transitions. Since that's mounted in `analytics/index.tsx`, this value can + * generally be trusted to be up to date. + */ +export function getSessionId() { + return device.get(['nativeSessionId']) +} + export function useSessionId() { const [id, setId] = useState(() => sessionId) diff --git a/src/analytics/identifiers/session.web.ts b/src/analytics/identifiers/session.web.ts index c2b2090cf0..c126a54cc1 100644 --- a/src/analytics/identifiers/session.web.ts +++ b/src/analytics/identifiers/session.web.ts @@ -21,6 +21,16 @@ export function getInitialSessionId() { return sessionId } +/** + * Gets the current session ID. Freshness depends on `useSessionId` being + * mounted, which handles refreshing this value between foreground/background + * transitions. Since that's mounted in `analytics/index.tsx`, this value can + * generally be trusted to be up to date. + */ +export function getSessionId() { + return window.sessionStorage.getItem(SESSION_ID_KEY) +} + export function useSessionId() { const [id, setId] = useState(() => sessionId) diff --git a/src/analytics/utils.ts b/src/analytics/utils.ts index 95d2efb589..cd7766bd9f 100644 --- a/src/analytics/utils.ts +++ b/src/analytics/utils.ts @@ -2,6 +2,7 @@ import {useMemo} from 'react' import {BSKY_SERVICE} from '#/lib/constants' import {type SessionAccount} from '#/state/session' +import {getDeviceId, getSessionId} from '#/analytics/identifiers' import { type MergeableMetadata, type SessionMetadata, @@ -31,3 +32,20 @@ export function accountToSessionMetadata( } } } + +/** + * Get anonymous identifiers and construct headers we use for requests that may + * trigger experiment exposures in our backend. These values are used for A/B + * test bucketing, in addition to the user DID, if the request is + * authenticated. They ensure we can attribute exposures to a specific device + * and session, even for unauthenticated requests. + * + * These headers must stay in sync with our appview. + * @see https://github.com/bluesky-social/atproto/blob/39cf199df5847d3fd4a60d8cdeb604a0e07f9784/packages/bsky/src/feature-gates/utils.ts#L7-L8 + */ +export function getAnalyticsHeaders() { + return { + 'X-Bsky-Device-Id': getDeviceId(), + 'X-Bsky-Session-Id': getSessionId(), + } +} diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 540325fc5b..69b600f912 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -9,7 +9,7 @@ import { getAppLanguageAsContentLanguage, getContentLanguages, } from '#/state/preferences/languages' -import {getDeviceId} from '#/analytics/identifiers' +import {getAnalyticsHeaders} from '#/analytics/utils' import {type FeedAPI, type FeedAPIResponse} from './types' import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' @@ -66,7 +66,7 @@ export class CustomFeedAPI implements FeedAPI { headers: isBlueskyOwned ? { ...createBskyTopicsHeader(this.userInterests), - 'X-Bsky-Stable-Id': getDeviceId(), + ...getAnalyticsHeaders(), 'Accept-Language': contentLangs, } : {