Skip to content

Add X-YVP-Sdk header and additionalHeaders override#237

Open
Kyleasmth wants to merge 4 commits into
mainfrom
ks/YPE-2295-react-sdk-add-x-yvp-sdk-http-header-for-version
Open

Add X-YVP-Sdk header and additionalHeaders override#237
Kyleasmth wants to merge 4 commits into
mainfrom
ks/YPE-2295-react-sdk-add-x-yvp-sdk-http-header-for-version

Conversation

@Kyleasmth
Copy link
Copy Markdown
Collaborator

@Kyleasmth Kyleasmth commented May 14, 2026

Summary

  • Sends X-YVP-Sdk: ReactSDK={version} on every API call alongside X-YVP-App-Key, so the data team can attribute traffic to this SDK.
  • Version is read from a single constant (packages/core/src/version.ts). Local/dev builds keep Dev so they're filterable in the data lake; the release script stamps the precise version from packages/core/package.json before the publishing build runs.
  • New additionalHeaders prop on YouVersionProvider (and additionalHeaders field on ApiConfig) merges into every request and overrides built-in headers on key collision. This unblocks the React Native Expo wrapper Cameron flagged on Teams — they can swap X-YVP-Sdk: ReactSDK=... for X-YVP-Sdk: ReactNativeSDK={their version}.

Implementation notes

  • Header added in both ApiClient.defaultHeaders (the main HTTP path) and YouVersionAPI.addStandardHeaders (the static helper, used by the PKCE auth flow). Both call sites that previously set X-YVP-App-Key now also set X-YVP-Sdk.
  • Override semantics: config.additionalHeaders is spread last into defaultHeaders, so caller-supplied values win.
  • Threaded through YouVersionContextuseBibleClient / useLanguageClient / useHighlights. Provider memoizes the headers object via JSON.stringify so inline object literals don't churn the hook memos.
  • Release-time stamping is a Node script invoked by the root release script, with turbo build --force to bypass the cache from the workflow's earlier (unstamped) build.

Known scope gap

Auth requests via SignInWithYouVersionPKCE use the static YouVersionPlatformConfiguration, not ApiConfig. They send X-YVP-Sdk correctly but don't honor a provider-level additionalHeaders override. Out of scope for this ticket auth is typically an OS-level redirect for the React Native case anyway.

Test plan

  • pnpm typecheck clean across all packages
  • pnpm lint clean across all packages
  • pnpm test — 510 tests pass (includes 5 new core cases and 1 new hooks case)
  • Manual: pnpm dev:web, open the Next.js example, hit any Bible API call, verify in DevTools → Network:
    • X-Yvp-Sdk: ReactSDK=Dev
    • X-Yvp-App-Key: <your key>
  • Manual override: add additionalHeaders={{ 'X-YVP-Sdk': 'ReactNativeSDK=test' }} to <YouVersionProvider> in the example app, confirm header value swaps on the wire
  • Release stamping: smoke-tested locally — node scripts/stamp-sdk-version.mjs reads packages/core/package.json version and rewrites packages/core/src/version.ts. The first published version after merge should land with the real semver instead of Dev.

Changeset

.changeset/x-yvp-sdk-header.md — minor bump for core / hooks / ui (kept in lockstep per fixed: config).

Greptile Summary

This PR adds X-YVP-Sdk: ReactSDK={version} to every API request for traffic attribution and introduces an additionalHeaders escape hatch on both ApiConfig and YouVersionProvider so SDK wrappers (e.g. the React Native Expo SDK) can override that header with their own identifier.

  • packages/core/src/version.ts is a new file that reads the version directly from package.json at build time and exports the header name, SDK name, and a builder function; a release-time stamping script is responsible for keeping package.json in sync before the publish build.
  • additionalHeaders is spread last into ApiClient.defaultHeaders, so caller-supplied values win on key collision; the same field is threaded through YouVersionContext to all three hook-built clients (useBibleClient, useLanguageClient, useHighlights).
  • YouVersionProvider stabilises the headers object reference via a useMemo keyed on a sorted JSON.stringify, preventing unnecessary ApiClient reconstructions from inline object literals.

Confidence Score: 5/5

The change is additive and backward-compatible: new headers and an optional override prop with no breaking changes to existing API surfaces.

All three hook-built clients consistently thread additionalHeaders through context; the memoisation approach correctly prevents spurious ApiClient reconstructions; tests cover both the header-on-the-wire path and the override semantics. The only open item (PKCE auth path not honouring additionalHeaders) is explicitly documented as out of scope.

No files require special attention; packages/core/src/version.ts is the only new file and its behaviour is well-covered by the existing test suite.

Important Files Changed

Filename Overview
packages/core/src/version.ts New file that reads SDK version directly from package.json and exports header name constants and a builder function; the Dev sentinel described in the PR description is not present in the code.
packages/core/src/client.ts Adds X-YVP-Sdk header and spreads additionalHeaders (last) into defaultHeaders so callers can override; implementation is straightforward and correct.
packages/hooks/src/context/YouVersionProvider.tsx Threads additionalHeaders through context with a stable-identity memo using sorted JSON.stringify as the dependency key, preventing unnecessary ApiClient reconstructions from inline object literals.
packages/hooks/src/useBibleClient.ts Passes context.additionalHeaders into ApiClient and adds it to the useMemo dependency array; consistent with useHighlights and useLanguageClient changes.
packages/core/src/tests/client.test.ts Integration tests verify X-YVP-Sdk and X-YVP-App-Key are sent on the wire, that additionalHeaders are included alongside built-ins, and that a caller can override X-YVP-Sdk.

Sequence Diagram

sequenceDiagram
    participant App
    participant YouVersionProvider
    participant YouVersionContext
    participant Hooks as useBibleClient/useHighlights/useLanguageClient
    participant ApiClient
    participant API as YouVersion API

    App->>YouVersionProvider: additionalHeaders prop
    YouVersionProvider->>YouVersionProvider: "stableAdditionalHeaders = useMemo(sorted JSON.stringify key)"
    YouVersionProvider->>YouVersionContext: "value { additionalHeaders: stableAdditionalHeaders }"
    YouVersionContext->>Hooks: context.additionalHeaders
    Hooks->>ApiClient: "new ApiClient({ additionalHeaders })"
    ApiClient->>ApiClient: "defaultHeaders = { Content-Type, X-YVP-App-Key, X-YVP-Sdk, ...additionalHeaders }"
    ApiClient->>API: GET/POST/DELETE + merged headers
    note over ApiClient,API: X-YVP-Sdk: ReactSDK={version} (or override value)

    note over App,YouVersionProvider: PKCE auth path (out of scope)
    App->>YouVersionAPI: addStandardHeaders(url)
    YouVersionAPI->>API: Request with X-YVP-Sdk (no additionalHeaders support)
Loading

Fix All in Claude Code Fix All in Cursor

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/core/src/version.ts:9-11
`buildSdkVersionHeaderValue` is called on every `ApiClient` construction and on every `YouVersionAPI.addStandardHeaders` invocation, but both `SDK_NAME` and `SDK_VERSION` are module-level constants, so the return value never changes after module initialisation. Replacing the function with an exported constant avoids the per-call overhead and makes the invariant explicit.

```suggestion
export const SDK_VERSION_HEADER_VALUE = `${SDK_NAME}=${SDK_VERSION}`;

/** @deprecated Use SDK_VERSION_HEADER_VALUE constant instead */
export function buildSdkVersionHeaderValue(): string {
  return SDK_VERSION_HEADER_VALUE;
}
```

Reviews (4): Last reviewed commit: "YPE-2295: Import SDK version from packag..." | Re-trigger Greptile

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 14, 2026

🦋 Changeset detected

Latest commit: 21e3152

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-core Minor
@youversion/platform-react-hooks Minor
@youversion/platform-react-ui Minor
nextjs Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Kyleasmth Kyleasmth requested a review from cameronapak May 14, 2026 18:46
@Kyleasmth Kyleasmth marked this pull request as ready for review May 14, 2026 18:46
greptile-apps[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Collaborator

@cameronapak cameronapak left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

cameronapak
cameronapak previously approved these changes May 15, 2026
Copy link
Copy Markdown
Collaborator

@cameronapak cameronapak left a comment

Choose a reason for hiding this comment

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

Everything looks good to me. I'm considering the script and if it's needed, but... nothing blocking. Good work!

Comment thread scripts/stamp-sdk-version.mjs Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thinking about this. Everything else is perfecto!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I asked DeepWiki... What do you think @Kyleasmth ?


Is this script necessary? A simpler alternative would be importing the version directly from package.json:

// packages/core/src/version.ts
import pkg from '../package.json' with { type: 'json' };
export const SDK_VERSION = pkg.version;

The bundler inlines the version at build time — no script, no regex, no release-pipeline coupling. This is the pattern most SDKs use (Stripe, Sentry, AWS, etc.).

The current approach has a few fragile bits:

  • The regex in scripts/stamp-sdk-version.mjs:29 is tied to the exact source formatting of version.ts — changing quotes, adding as const, or reformatting could break it.
    version.ts is left dirty on disk after a local pnpm release run (already noted in the Greptile summary).
  • Requires turbo build --force to bust the cache from the prior unstamped build.
  • Implicit ordering dependency: only works because Changesets bumps packages/core/package.json before release runs.

Stamping would be justified if the version had to land somewhere a bundler can't reach (a Markdown file, a non-JS asset), but version.ts is plain TS consumed by the same bundler as the rest of core — so the JSON import should work cleanly and removes ~50 lines of release-pipeline glue.

Replace the fragile stamp-sdk-version.mjs script with tsup's define plugin,
which injects the version from package.json at build time. This approach:
- Eliminates regex fragility
- Removes the need for --force rebuilds
- Matches the pattern used by Stripe, Sentry, AWS SDKs
- Simplifies the release pipeline

The version is injected at build time and falls back to 'Dev' at runtime
for test environments.
Comment thread packages/core/tsup.config.ts Outdated
@Kyleasmth Kyleasmth force-pushed the ks/YPE-2295-react-sdk-add-x-yvp-sdk-http-header-for-version branch from d96d92c to 21e3152 Compare May 15, 2026 18:28
Copy link
Copy Markdown
Collaborator

@cameronapak cameronapak left a comment

Choose a reason for hiding this comment

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

LGTM!

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.

2 participants