Skip to content

feat: Upwards-recursively read .braintrust.json containing BRAINTRUST_API_KEY#166

Open
Luca Forstner (lforst) wants to merge 5 commits into
mainfrom
lforst/env-braintrust-api-key
Open

feat: Upwards-recursively read .braintrust.json containing BRAINTRUST_API_KEY#166
Luca Forstner (lforst) wants to merge 5 commits into
mainfrom
lforst/env-braintrust-api-key

Conversation

@lforst

Copy link
Copy Markdown
Member

Direct port of braintrustdata/braintrust-sdk-javascript#2049

Prompt used:

Implement `.env.braintrust` API key discovery for this Braintrust SDK.

Context:
The Braintrust instrumentation wizard will generate a file named `.env.braintrust` in the user’s current working directory. That file contains a dotenv-style `BRAINTRUST_API_KEY=...` entry. The goal is that after running the wizard, users can immediately run or verify local Braintrust instrumentation without manually exporting `BRAINTRUST_API_KEY`.

`.env.braintrust` is not a general dotenv loader. It is only a credential fallback for Braintrust SDK API-key lookup.

Required behavior:
- Preserve precedence:
  1. Explicit caller-provided API key wins.
  2. Nonblank `BRAINTRUST_API_KEY` from the process environment wins.
  3. Otherwise, read `BRAINTRUST_API_KEY` from `.env.braintrust`.
- Treat missing, empty, or whitespace-only environment values as unset.
- Look for `.env.braintrust` starting at the current working directory at lookup time, then walk upward.
- Cap lookup at cwd plus 64 parent directories.
- The nearest `.env.braintrust` wins.
- The nearest `.env.braintrust` is a boundary: if it exists but has no nonblank `BRAINTRUST_API_KEY`, do not check higher parents.
- If the nearest `.env.braintrust` cannot be read, return “not found” and do not check higher parents. Do not throw from credential discovery.
- Read only `BRAINTRUST_API_KEY`; do not load or expose other variables from the file.
- Do not mutate the process environment.
- Support normal dotenv syntax if the SDK/runtime has a standard parser: quotes, comments, and `export BRAINTRUST_API_KEY=...`.

Implementation guidance:
- Prefer lazy, nonblocking lookup.
- If possible, start candidate file reads in parallel, but preserve nearest-wins semantics: a higher parent may only win after all closer candidates are known absent.
- If a constructor/setup path is synchronous, do not add blocking file IO there.
- For telemetry/exporter integrations, make export/flush wait for API-key discovery when needed.

Look at the reference implementation PR: https://github.com/braintrustdata/braintrust-sdk-javascript/pull/2049

@lforst Luca Forstner (lforst) marked this pull request as ready for review May 27, 2026 11:19
@lforst

Copy link
Copy Markdown
Member Author

Discussed offline: Will move to reading JSON instead

@lforst Luca Forstner (lforst) changed the title feat: Upwards-recursively read .env.braintrust containing BRAINTRUST_API_KEY feat: Upwards-recursively read .braintrust.json containing BRAINTRUST_API_KEY Jun 1, 2026
@lforst

Copy link
Copy Markdown
Member Author

David Elner (@delner) as discussed, updated the PR to use a .braintrust.json file instead

@delner David Elner (delner) left a comment

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.

Overall, this iteration of this PR is a huge improvement over the last: not needing to add our own file parsing, elimination of threading both represent major simplifications that I think this needed. Kudos!

The guts of this seem fine. Before I approve though, I want to understand the strategic thinking a bit better. Right now there's some rough edges around Ruby convention and possible breakage of previous happy paths? (Where it previously operated without raising exceptions.) I think a little more context will help me assess this better.

Comment thread lib/braintrust/state.rb Outdated
end
end

def require_api_key

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.

This is awkward and non-idiomatic to Ruby. require suggests "loading", and I think it's unexpected that accessing this field would raise an error. Conventionally, if you want to signal an operation raises errors or mutates state, add a !, e.g. api_key!. This would be more acceptable.

Before we go that route: can you explain more of the background of why we want to separate the required vs non-required paths? Was the goal just not to repeat nil checks and error raising? Why are some call sites updated and not others?

Also a nit: I think key = @api_key is superfluous, and you could just use the instance variable directly in this case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Wo the distinction is that some code paths should not throw when attempting to look at the api_key, eg. Config.from_env, and some should indeed throw, like state.login.

Updated the non-idiomaticisms!

@lforst Luca Forstner (lforst) requested a review from a team as a code owner June 5, 2026 09:56
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.

3 participants