Skip to content

feat: Validation for ai-configs and observability slices#428

Open
kinyoklion wants to merge 3 commits into
mainfrom
rlamb/aiconfig-observability-validation
Open

feat: Validation for ai-configs and observability slices#428
kinyoklion wants to merge 3 commits into
mainfrom
rlamb/aiconfig-observability-validation

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented May 8, 2026

Summary

Wires validation: blocks for the 25 ai-configs and 23 observability snippets that PRs #424 and #425 ported from gonfalon. Coverage delta:

  • ai-configs: 0/25 → 24/25 bound (1 Bucket C).
  • observability: 0/23 → 23/23 bound (0 Bucket C).

Single PR against main. The first commit (a2253af) wired ai-configs end-to-end and the install + node/python observability imports (13/23 observability). The follow-up commit on this PR wires the remaining 10 observability snippets (4 frontend imports + 6 init-runners).

Per-SDK before / after

ai-configs

SDK Before After
dotnet-server-sdk 0/5 4/5 (install Bucket C — PowerShell Install-Package cmdlets)
go-server-sdk 0/5 5/5
node-server-sdk 0/5 5/5
python-server-sdk 0/5 5/5
ruby-server-sdk 0/5 5/5

observability

SDK Before After
js-client-sdk 0/4 4/4 (install npm/yarn + import + initialize)
node-server-sdk 0/4 4/4
python-server-sdk 0/3 3/3
react-client-sdk 0/4 4/4
react-native-client-sdk 0/4 4/4
vue-client-sdk 0/4 4/4

Strategy

First commit (a2253af) — already merged into branch

  • Install snippets (npm/yarn/pip/go-get/gem): bound to the existing shell-install validator. Multi-line installs (e.g. npm install <sdk> + npm install <addon>) needed a fix to the last_pkg awk extractor — the old form emitted one package per line, breaking the post-install assertion. Now folds across lines and asserts on the last package the body installs.
  • ai-configs context / implementation / import / initialize (code fragments): bound to the per-SDK *-syntax-only scaffold. Compilation/parse-only matches the user brief ("compilation for now"). The dotnet csharp-syntax-only scaffold gained using LaunchDarkly.Sdk.Server.Ai* imports + LaunchDarkly.ServerSdk.Ai requirement + a dynamic aiClient stub field so cross-snippet references resolve.
  • observability import (node-server, python only): bound to syntax-only. node --check and ast.parse are purely syntactic and don't need the o11y package on disk.

Follow-up commit — finishes Bucket C observability

  • Frontend observability/import (js-client, react-client, react-native, vue-client): each frontend validator's Dockerfile now pre-bakes @launchdarkly/observability + @launchdarkly/session-replay (or @launchdarkly/observability-react-native for RN). The js-client and react-native syntax-only scaffolds gained //IMPORT_LIFT_TARGET / //BODY_BEGIN / //BODY_END markers and the matching awk pre-step in their harnesses (mirroring the existing react-client + flutter pattern), so install-time import directives lift out of the never-invoked wrappee function up to module scope where ESM allows them. The vue scaffold (already at module scope) gained a queueMicrotask fallback that surfaces the EXAM-HELLO line for import-only bodies that don't themselves mount.
    • The react-native syntax-only scaffold previously wrote App.js while the harness expected App.tsx; this commit rewrites the scaffold to emit App.tsx and adds a SNIPPET_MODE=syntax-only dispatch in the harness that bypasses jest and runs Babel parse instead.
  • All 6 observability/initialize: each SDK gains a companion init-runner-observability scaffold that:
    1. Prepends the matching observability/import snippet's symbols at module scope.
    2. Maps the body's 'SDK_KEY' literal via validation.placeholders to the appropriate env var (LAUNCHDARKLY_SDK_KEY for server, LAUNCHDARKLY_CLIENT_SIDE_ID for browser, LAUNCHDARKLY_MOBILE_KEY for RN).
    3. Awaits the SDK's waitForInitialization() (or the equivalent — Python uses is_initialized() polling, React uses LDProvider-rendered DOM, Vue uses app.mount('#app') returning) and emits the EXAM-HELLO line on success.
    • Per the brief, success = the SDK starts cleanly with the o11y plugin attached. We don't assert that traces / events land in LD.

Validator changes

shell-install (first commit)

  • Dockerfile: layer ruby + ruby-dev + build-essential + zlib1g-dev + libssl-dev + libyaml-dev + libffi-dev so gem install works for the ruby-server-sdk ai-configs install snippet.
  • harness/run.sh: add a gem) case that routes installs into a per-run GEM_HOME (no root needed) and asserts via <GEM_HOME>/gems/<pkg>-<version> directory existence. Fix last_pkg to fold across multi-line bodies via an END block.

Follow-up commit

  • validators/languages/js-client/Dockerfile: add launchdarkly-js-client-sdk (legacy v3, used by the observability/import body), @launchdarkly/observability, and @launchdarkly/session-replay to the pre-baked dep tree. Switch tsdown output to format: 'iife' with entryFileNames: 'app.js' and noExternal: [/.*/] so the bundle resolves the o11y packages strictly and produces a script-tag-loadable IIFE (the previous ESM output choked the file:// <script> tag once the bundle started exporting symbols from the bundled o11y deps).
  • validators/languages/js-client/harness/run.sh: add the IMPORT_LIFT awk pre-step (mirror of react-client) so wrappees with body-internal import lines lift them to module scope.
  • validators/languages/react-client/Dockerfile: add launchdarkly-react-client-sdk (legacy v3), @launchdarkly/observability, @launchdarkly/session-replay.
  • validators/languages/vue-client/Dockerfile: add @launchdarkly/observability, @launchdarkly/session-replay.
  • validators/languages/react-native-client/Dockerfile: add @launchdarkly/observability-react-native.
  • validators/languages/react-native-client/harness/run.sh: add SNIPPET_MODE=syntax-only dispatch — runs Babel parse on the staged App.tsx and exits before the jest end-to-end run. Used for parse-only doc fragments (the new observability/import) that aren't standalone-runnable.

Defects fixed inline

First commit (a2253af) — dotnet ai-configs

  1. dotnet-server-sdk/ai-configs/context — body called .LastName(...), .Email(...), .Groups([...]) on ContextBuilder. None of those methods exist on the .NET SDK's ContextBuilder type — the v7+ surface uses .Set(name, value) for custom attributes, with LdValue.ArrayOf(...) for array values. Rewritten to use the canonical Set API.
  2. dotnet-server-sdk/ai-configs/implementation — body called .AddMessage("", Role.system). The .NET enum's member is Role.System (PascalCase, per .NET conventions); Role.system doesn't compile. Renamed.

Follow-up commit — js-client observability + adjacent snippets

  1. js-client-sdk/observability/import — body had import { LDClient } from 'launchdarkly-js-client-sdk', a named import of the LDClient type-only interface. The companion observability/initialize body then calls LDClient.initialize(...) at runtime, which fails because LDClient was never bound to a value. Switched to the default import (import LDClient from 'launchdarkly-js-client-sdk') so LDClient.initialize resolves to the SDK's LaunchDarkly default-export object at runtime.
  2. js-client-sdk/observability/initialize — body called LDClient.initialize('SDK_KEY', { plugins: [...] }), missing the required context argument (the SDK's initialize(envKey, context, options) signature has been positional-context since v3). Added const context = { kind: 'user', key: 'EXAMPLE_CONTEXT_KEY' }; and passed it through.
  3. js-client-sdk/sdk-docs/use-promises-to-determine-when-the-client-is-ready-javascript-sdk-v4-x-typescript (pre-existing, surfaced by the new bundler-strict mode) — body had import { Observability } / import { SessionReplay } (both default exports of their packages, not named) and import { initialize, LDClient } from @launchdarkly/js-client-sdk (v4 dropped the initialize named export in favor of createClient(envKey, context, options)). Switched both o11y imports to default-import shape, replaced initialize with createClient, and added an explicit context argument. The snippet was passing previously only because tsdown was configured to leave o11y packages as external (with no node_modules entry); the new strict-bundling config caught the latent bug.

Bucket C entries (deferred)

Documented in snippets/sdks/_aiconfigs-observability-port-notes.md:

  • dotnet-server-sdk/ai-configs/install (NuGet PowerShell cmdlets — same shape as the sdk-info install-csharp snippet already documented in _sdk-info-port-notes.md).

All other observability snippets are now bound.

Test plan

  • go test ./... passes
  • All 6 observability/initialize snippets stage + build through the validator end-to-end (final auth fails with the local fake-key fixture, expected behavior — CI runs with real keys)
  • All 4 frontend observability/import snippets pass parse-only validation
  • All previously-passing snippets continue to pass (regression-checked the js-client and vue and react syntax-only consumers, plus the existing init-runners)
  • CI matrix runs the new bindings end-to-end against the LD sandbox env

Note

Medium Risk
Medium risk because it updates multiple language validator Docker images and harness scripts (import-lifting, bundling, new toolchains), which can break snippet validation across many SDKs if assumptions change.

Overview
Adds validation coverage for the newly ported ai-configs/ and observability/ snippet sets by wiring validation: blocks to existing syntax-only scaffolds and to a broadened shell-install validator.

Introduces new init-runner scaffolds for observability across JS client, Node, Python, React, React Native, and Vue to run SDK initialization with observability plugins using env-driven placeholders, plus updates JS/React/React Native/Vue validators (Docker deps, bundling settings, and import-lifting preprocessors) so import-only and ESM snippets compile/parse correctly.

Extends shell-install to support multi-line install bodies and gem install, updates the .NET C# syntax scaffold to include AI Config dependencies/stubs, fixes a few snippet bodies to match real SDK APIs (notably .NET context builder and JS client observability initialization), and documents the one remaining unvalidated snippet in _aiconfigs-observability-port-notes.md.

Reviewed by Cursor Bugbot for commit 125835f. Bugbot is set up for automated code reviews on this repo. Configure here.

- ai-configs (5 SDKs × 5 snippets = 25): bind context, implementation,
  import, initialize bodies to per-SDK syntax-only scaffolds; bind
  install bodies to the shell-install validator. Extend
  csharp-syntax-only with the AI namespaces and stub aiClient field
  so dotnet bodies compile against the LaunchDarkly.ServerSdk.Ai
  package.
- observability (6 SDKs): bind every install body (npm/yarn/pip) to
  shell-install. Bind node-server and python-server import bodies to
  their syntax-only scaffolds (parsing-only is sufficient — node
  --check and ast.parse don't try to resolve import paths).
- shell-install: support multi-line install bodies (the previous
  last_pkg awk emitted one package per line, breaking the
  post-install assertion when a body installs the SDK and an addon
  in two commands). Add gem support so ruby-server-sdk installs bind
  cleanly; the per-run GEM_HOME keeps installs root-free and
  collision-free.
- Frontend observability/import (js, react, vue, react-native) and
  every observability/initialize body documented as Bucket C in
  `_aiconfigs-observability-port-notes.md` along with the dotnet
  ai-configs install (PowerShell `Install-Package` cmdlets, same
  shape as the sdk-info install-csharp.txt that's already
  documented in `_sdk-info-port-notes.md`).
- Fixed two real defects in the dotnet-server-sdk ai-configs bodies
  found during validation: `Context.Builder().LastName().Email().Groups()`
  used non-existent ContextBuilder methods (replaced with `.Set("lastName",
  ...)` etc.); `Role.system` used the wrong case (renamed to
  `Role.System`).

Coverage delta:
- ai-configs: 0/25 → 24/25 bound (1 Bucket C: dotnet PowerShell install).
- observability: 0/23 → 13/23 bound (10 Bucket C: 4 frontend imports + 6
  end-to-end initializes).
Finishes the observability slice (was 13/23 in the previous commit)
by binding the 4 frontend `observability/import` fragments and all 6
`observability/initialize` fragments to per-SDK validation harnesses.
Coverage delta: 13/23 → 23/23 observability snippets bound; 0 Bucket C
remaining for observability.

Frontend imports (js, react, vue, react-native):
  - Pre-bake `@launchdarkly/observability` + `@launchdarkly/session-replay`
    (or `@launchdarkly/observability-react-native` for RN) in each
    validator's Dockerfile so the bundler can resolve the imports.
  - Extend the js-client and react-native syntax-only scaffolds with
    `//IMPORT_LIFT_TARGET` / `//BODY_BEGIN` / `//BODY_END` markers and
    add the matching awk pre-step in each harness, mirroring the
    existing react-client + flutter pattern. ESM forbids `import`
    inside a function body, so the install-time imports lift to
    module scope.
  - Rewrite react-native-syntax-only to emit App.tsx (was App.js) so
    the harness can find it; add a `SNIPPET_MODE=syntax-only`
    dispatch in the harness that runs Babel parse instead of jest.
  - Vue scaffold gains a queueMicrotask fallback so import-only bodies
    that don't themselves mount still surface the EXAM-HELLO line.

Initialize runners (all 6 SDKs):
  - New `init-runner-observability` companion scaffold per SDK.
  - Each scaffold supplies the matching `observability/import`
    snippet's symbols at module scope (init/createApp/createClient/
    withLDProvider/ReactNativeLDClient + Observability/SessionReplay).
  - Wrappee body's `'SDK_KEY'` literal substituted via
    `validation.placeholders` to LAUNCHDARKLY_SDK_KEY (server),
    LAUNCHDARKLY_CLIENT_SIDE_ID (browser), or LAUNCHDARKLY_MOBILE_KEY
    (RN).
  - Scaffold awaits the SDK's waitForInitialization() (or polls
    is_initialized() for Python, watches the LDProvider-rendered DOM
    for React, watches `app.mount()` for Vue) and emits the
    EXAM-HELLO line on success. Per the brief, success = SDK starts
    cleanly with the o11y plugin attached; we don't assert that
    traces / events land in LD.

js-client validator Dockerfile changes:
  - Add launchdarkly-js-client-sdk (legacy v3) alongside
    @launchdarkly/js-client-sdk; the observability snippet bodies use
    the v3 namespace shape.
  - Switch tsdown to `format: 'iife'` + `entryFileNames: 'app.js'`
    + `noExternal: [/.*/]` so the script-tag-loaded bundle resolves
    all imports strictly and produces non-ESM output (the previous
    config left o11y packages as external imports the file:// page
    couldn't resolve).

Defects fixed inline:
  - js-client-sdk/observability/import: body had
    `import { LDClient } from 'launchdarkly-js-client-sdk'` (named
    import of a type-only interface). The companion initialize body
    then calls `LDClient.initialize(...)` at runtime, which fails
    because LDClient was never bound to a value. Switched to the
    default import so LDClient resolves to the v3 LaunchDarkly
    namespace object.
  - js-client-sdk/observability/initialize: body called
    `LDClient.initialize('SDK_KEY', { plugins: [...] })`, missing
    the required context argument. Added an EXAMPLE_CONTEXT_KEY
    user context.
  - js-client-sdk/sdk-docs/use-promises-...-v4-x-typescript: pre-
    existing snippet that surfaced once the bundler resolved imports
    strictly. `import { Observability }` and `import { SessionReplay }`
    were named imports of default exports; `import { initialize }`
    from `@launchdarkly/js-client-sdk` v4 doesn't exist (v4 renamed
    to `createClient`). Switched the o11y imports to default-import
    shape, replaced `initialize` with `createClient`, added the
    missing context argument.

vue-client init-runner: leading semicolon before the IIFE so ASI
doesn't try to call the result of the body's final
`app.mount('#app')` (the body has no trailing semicolon). Same fix
applied to the new init-runner-observability scaffold.

Update _aiconfigs-observability-port-notes.md to drop the two
resolved Bucket C entries; only the dotnet ai-configs install
fragment remains documented.
…ingle-line imports

The previous round's IMPORT_LIFT awk pre-step in
validators/languages/react-client/harness/run.sh only recognized
single-line imports terminated by `;`. Imports without a trailing
semicolon (`import Observability, { LDObserve } from
'@launchdarkly/observability'`) entered `in_multi_import` mode, never
found a `;`, and were silently dropped from the lifted output.
Validation passed because the wrappee body's `if (false) { … }` block
parsed fine, but two of three observability imports in
react-client-sdk/observability/import were never actually exercised.

The js-client and react-native-client harnesses already checked
`/;[ \t]*$/ || /[\x27"][ \t]*$/` for the single-line case;
react-client wasn't updated. Bring it in line.

Multi-line continuation handling stays `;`-only — a genuine multi-line
import `import {\n  foo,\n} from 'pkg'` still needs the terminator,
which is the conventional shape.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default mode and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 125835f. Configure here.

MODE="${SNIPPET_MODE:-init}"
if [ "$MODE" = "init" ]; then
require_env LAUNCHDARKLY_MOBILE_KEY
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flag key required unnecessarily in syntax-only mode

Low Severity

require_env LAUNCHDARKLY_FLAG_KEY runs unconditionally on line 24, but this variable is only used by the jest test path (init mode). The developer correctly moved LAUNCHDARKLY_MOBILE_KEY into the if [ "$MODE" = "init" ] conditional but forgot to do the same for LAUNCHDARKLY_FLAG_KEY. In syntax-only mode, validation will fail with a misleading "LAUNCHDARKLY_FLAG_KEY not set" error even though that variable is completely unused by the Babel parse path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 125835f. Configure here.

}
' "$ENTRY_FILE" > /tmp/lifted.ts
mv /tmp/lifted.ts "$ENTRY_FILE"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMPORT_LIFT awk script duplicated across three harnesses

Low Severity

The ~60-line IMPORT_LIFT awk script is now identically duplicated across three harnesses (js-client, react-native-client, and the existing react-client). A shared lib.sh mechanism already exists at validators/shared/lib.sh and is mounted into each container as /harness-shared/lib.sh. Extracting this awk logic into a shared helper would eliminate the need to synchronize bug fixes across three copies.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 125835f. Configure here.

kinyoklion added a commit that referenced this pull request May 8, 2026
Adds 12 canonical .snippet.md files under
snippets/sdks/<sdk>/snippets/sdk-docs/ for the install/manifest
CodeBlocks in ld-docs's top-level reference pages
(`fern/topics/sdk/{server-side,client-side}/<sdk>/index.mdx`) that
the original auto-port (sdk-meta PR #414) skipped per the
shell/bash/cmd skip rule (SDK-2300).

These snippets unblock ld-docs-private PR #7592, which can insert
markers pointing at them in a follow-up commit once this lands and
a new `snippets/v*` is cut.

Per-SDK file count:

| SDK | snippet IDs |
|---|---|
| dotnet-server-sdk | install-the-sdk-shell |
| java-server-sdk | install-the-sdk-gradle |
| node-server-sdk | install-the-sdk-shell |
| php-server-sdk | install-the-sdk-shell |
| ruby-server-sdk | install-the-sdk-shell |
| rust-server-sdk | install-the-sdk-shell |
| node-client-sdk | install-the-sdk-installing-with-npm, install-the-sdk-installing-with-yarn |
| react-native-client-sdk | install-the-sdk-installing-react-native-sdk-v10, install-the-sdk-adding-the-async-storage-dependency |
| vue-client-sdk | install-the-sdk-installing-with-npm, install-the-sdk-installing-with-yarn |

`go-server-sdk` is intentionally not in the list: the "Shell install"
mention in PR #7592's per-SDK table refers to a prose backtick quote
of `go get …`, not an MDX CodeBlock — there's nothing to canonicalize.

Bodies are byte-verbatim copies of what's in ld-docs today; verified
by running `snippets render --target=ld-docs` after a synthetic
marker insertion and confirming a zero-body-diff.

No `validation:` block: these are install/manifest fragments, not
runnable units. Adding shell-install or build-system-harness
validation for them is a separate workstream (PR #428's territory).

Replaces the originally-opened-as-9-separate-PRs sequence
(#430-#438) with a single PR per the user's
intent.
kinyoklion added a commit that referenced this pull request May 8, 2026
Adds 12 canonical .snippet.md files under
snippets/sdks/<sdk>/snippets/sdk-docs/ for the install/manifest
CodeBlocks in ld-docs's top-level reference pages
(`fern/topics/sdk/{server-side,client-side}/<sdk>/index.mdx`) that
the original auto-port (sdk-meta PR #414) skipped per the
shell/bash/cmd skip rule (SDK-2300).

These snippets unblock ld-docs-private PR #7592, which can insert
markers pointing at them in a follow-up commit once this lands and
a new `snippets/v*` is cut.

Per-SDK file count:

| SDK | snippet IDs |
|---|---|
| dotnet-server-sdk | install-the-sdk-shell |
| java-server-sdk | install-the-sdk-gradle |
| node-server-sdk | install-the-sdk-shell |
| php-server-sdk | install-the-sdk-shell |
| ruby-server-sdk | install-the-sdk-shell |
| rust-server-sdk | install-the-sdk-shell |
| node-client-sdk | install-the-sdk-installing-with-npm, install-the-sdk-installing-with-yarn |
| react-native-client-sdk | install-the-sdk-installing-react-native-sdk-v10, install-the-sdk-adding-the-async-storage-dependency |
| vue-client-sdk | install-the-sdk-installing-with-npm, install-the-sdk-installing-with-yarn |

`go-server-sdk` is intentionally not in the list: the "Shell install"
mention in PR #7592's per-SDK table refers to a prose backtick quote
of `go get …`, not an MDX CodeBlock — there's nothing to canonicalize.

Bodies are byte-verbatim copies of what's in ld-docs today; verified
by running `snippets render --target=ld-docs` after a synthetic
marker insertion and confirming a zero-body-diff.

No `validation:` block: these are install/manifest fragments, not
runnable units. Adding shell-install or build-system-harness
validation for them is a separate workstream (PR #428's territory).

Replaces the originally-opened-as-9-separate-PRs sequence
(#430-#438) with a single PR per the user's
intent.
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