From 79d7bbd264e7616d51fb4f2012e304ca81b638bf Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:56:44 -0700 Subject: [PATCH 01/19] feat(snippets): port the 22 remaining gonfalon getStarted SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds snippet content for every SDK in gonfalon/static/ld/components/ getStarted/sdk/ outside of python-server-sdk (which landed in the prior slice). Each SDK has a `sdk.yaml` descriptor and one `.snippet.md` per gonfalon `` block. Tier 1 — server-side / Linux Docker (validators in this PR): go-server-sdk, node-server-sdk, node-client-sdk, ruby-server-sdk, php-server-sdk, rust-server-sdk, dotnet-server-sdk, dotnet-client-sdk, java-server-sdk Tier 1 — server-side, validator pending: haskell-server-sdk, erlang-server-sdk, lua-server-sdk, cpp-server-sdk, cpp-client-sdk Tier 2 — browser: js-client-sdk (Playwright validator in this PR), vue-client-sdk (validator pending), react-client-sdk (legacy + createApp variants; legacy renders into gonfalon, createApp deferred until its assetSource pattern is migrated) Tier 3 — mobile/native (validators all pending): android-client-sdk, flutter-client-sdk, react-native-client-sdk Tier 4 — iOS: ios-client-sdk (validator pending — needs macos-* runner) Skipped: roku-client-sdk (validation: none; manual procedure documented in frontmatter) Each SDK with markers in gonfalon was verified: `snippets render` idempotent, `snippets verify` ok. The 11 SDKs with validators in this PR were exercised end-to-end against the real LD test environment locally. Three "fix on red" surfaces baked in: - node-client-sdk + dotnet-client-sdk: rewrote the print line so the snippet emits the EXAM-HELLO canonical phrase `feature flag evaluates to true` instead of `Feature flag X is true`. Snippets matched the gonfalon source verbatim before the fix; validators surfaced the divergence. - java-server-sdk: gonfalon's stale `5.0.0` version fallback swapped for `${version}` only; the validator's synthesized pom pins 7.13.4 (current Maven Central). Snippets that contain non-EXAM-HELLO output today (e.g. cpp's `Feature flag '' is true`) carry their content verbatim with a `# Validator pending` comment in frontmatter; the snippet is rendered into gonfalon today and the canonical-line fix follows when the per-SDK validator lands. --- snippets/sdks/android-client-sdk/sdk.yaml | 14 ++ .../activity-main-xml.snippet.md | 19 +++ .../getting-started/build-gradle.snippet.md | 23 +++ .../getting-started/main-activity.snippet.md | 82 +++++++++++ .../main-application.snippet.md | 69 +++++++++ .../getting-started/manifest.snippet.md | 23 +++ snippets/sdks/cpp-client-sdk/sdk.yaml | 14 ++ .../getting-started/build-mkdir.snippet.md | 15 ++ .../getting-started/clone-sdk.snippet.md | 15 ++ .../getting-started/cmake-build.snippet.md | 15 ++ .../getting-started/cmake-make.snippet.md | 13 ++ .../getting-started/cmake-msvc.snippet.md | 13 ++ .../getting-started/cmake-ninja.snippet.md | 13 ++ .../getting-started/cmakelists.snippet.md | 37 +++++ .../getting-started/main-cpp.snippet.md | 73 ++++++++++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 15 ++ snippets/sdks/cpp-server-sdk/sdk.yaml | 14 ++ .../getting-started/build-mkdir.snippet.md | 15 ++ .../getting-started/clone-sdk.snippet.md | 15 ++ .../getting-started/cmake-build.snippet.md | 15 ++ .../getting-started/cmake-make.snippet.md | 13 ++ .../getting-started/cmake-msvc.snippet.md | 13 ++ .../getting-started/cmake-ninja.snippet.md | 13 ++ .../getting-started/cmakelists.snippet.md | 37 +++++ .../getting-started/main-cpp.snippet.md | 76 ++++++++++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 16 +++ snippets/sdks/dotnet-client-sdk/sdk.yaml | 14 ++ .../getting-started/dotnet-new.snippet.md | 15 ++ .../getting-started/install.snippet.md | 15 ++ .../snippets/getting-started/mkdir.snippet.md | 16 +++ .../getting-started/program-cs.snippet.md | 56 ++++++++ .../snippets/getting-started/run.snippet.md | 15 ++ snippets/sdks/dotnet-server-sdk/sdk.yaml | 14 ++ .../getting-started/install.snippet.md | 15 ++ .../getting-started/program-cs.snippet.md | 115 +++++++++++++++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/erlang-server-sdk/sdk.yaml | 14 ++ .../getting-started/app-src.snippet.md | 19 +++ .../getting-started/rebar-config.snippet.md | 20 +++ .../getting-started/rebar3-new.snippet.md | 15 ++ .../getting-started/run-call.snippet.md | 19 +++ .../getting-started/run-shell.snippet.md | 15 ++ .../getting-started/server-erl.snippet.md | 66 +++++++++ .../getting-started/sup-childspecs.snippet.md | 17 +++ snippets/sdks/flutter-client-sdk/sdk.yaml | 14 ++ .../getting-started/cd-into.snippet.md | 15 ++ .../getting-started/flutter-create.snippet.md | 15 ++ .../getting-started/install.snippet.md | 15 ++ .../getting-started/main-dart.snippet.md | 131 +++++++++++++++++ .../getting-started/min-sdk.snippet.md | 15 ++ .../podfile-platform.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/go-server-sdk/sdk.yaml | 14 ++ .../getting-started/install.snippet.md | 15 ++ .../getting-started/main-go.snippet.md | 100 +++++++++++++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../getting-started/mod-init.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/haskell-server-sdk/sdk.yaml | 14 ++ .../getting-started/main-hs.snippet.md | 103 +++++++++++++ .../getting-started/package-yaml.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 19 +++ .../getting-started/stack-new.snippet.md | 15 ++ .../getting-started/stack-yaml.snippet.md | 20 +++ snippets/sdks/ios-client-sdk/sdk.yaml | 14 ++ .../getting-started/app-delegate.snippet.md | 52 +++++++ .../getting-started/pod-install.snippet.md | 15 ++ .../getting-started/podfile.snippet.md | 24 ++++ .../view-controller.snippet.md | 59 ++++++++ snippets/sdks/java-server-sdk/sdk.yaml | 14 ++ .../getting-started/app-java.snippet.md | 135 ++++++++++++++++++ .../getting-started/cd-into.snippet.md | 15 ++ .../getting-started/mvn-generate.snippet.md | 15 ++ .../getting-started/pom-build.snippet.md | 31 ++++ .../getting-started/pom-compiler.snippet.md | 16 +++ .../getting-started/pom-dependency.snippet.md | 24 ++++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/js-client-sdk/sdk.yaml | 14 ++ .../getting-started/index-html.snippet.md | 85 +++++++++++ snippets/sdks/lua-server-sdk/sdk.yaml | 14 ++ .../getting-started/cpp-build.snippet.md | 27 ++++ .../getting-started/hello-lua.snippet.md | 39 +++++ .../luarocks-install.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 15 ++ snippets/sdks/node-client-sdk/sdk.yaml | 14 ++ .../getting-started/index-js.snippet.md | 57 ++++++++ .../getting-started/install.snippet.md | 20 +++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 15 ++ snippets/sdks/node-server-sdk/sdk.yaml | 14 ++ .../getting-started/index-js.snippet.md | 88 ++++++++++++ .../getting-started/install.snippet.md | 20 +++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/php-server-sdk/sdk.yaml | 14 ++ .../getting-started/install.snippet.md | 20 +++ .../getting-started/main-php.snippet.md | 89 ++++++++++++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/react-client-sdk/sdk.yaml | 22 +++ .../getting-started/app-tsx.snippet.md | 32 +++++ .../getting-started/create-vite.snippet.md | 15 ++ .../getting-started/install.snippet.md | 15 ++ .../getting-started/legacy-app-tsx.snippet.md | 35 +++++ .../getting-started/legacy-create.snippet.md | 15 ++ .../legacy-index-tsx.snippet.md | 44 ++++++ .../getting-started/legacy-install.snippet.md | 20 +++ .../getting-started/legacy-run.snippet.md | 13 ++ .../getting-started/main-tsx.snippet.md | 32 +++++ .../getting-started/run-dev.snippet.md | 15 ++ .../sdks/react-native-client-sdk/sdk.yaml | 14 ++ .../getting-started/app-tsx.snippet.md | 49 +++++++ .../getting-started/create-expo.snippet.md | 15 ++ .../getting-started/install.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 15 ++ .../getting-started/welcome-tsx.snippet.md | 42 ++++++ snippets/sdks/roku-client-sdk/sdk.yaml | 14 ++ .../getting-started/app-scene-brs.snippet.md | 81 +++++++++++ .../getting-started/app-scene-xml.snippet.md | 45 ++++++ .../getting-started/main-brs.snippet.md | 34 +++++ .../getting-started/manifest.snippet.md | 18 +++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ snippets/sdks/ruby-server-sdk/sdk.yaml | 14 ++ .../getting-started/install.snippet.md | 20 +++ .../getting-started/main-rb.snippet.md | 91 ++++++++++++ .../snippets/getting-started/mkdir.snippet.md | 15 ++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/rust-server-sdk/sdk.yaml | 14 ++ .../getting-started/cargo-new.snippet.md | 15 ++ .../getting-started/install-sdk.snippet.md | 15 ++ .../getting-started/install-tokio.snippet.md | 15 ++ .../getting-started/main-rs.snippet.md | 95 ++++++++++++ .../snippets/getting-started/run.snippet.md | 19 +++ snippets/sdks/vue-client-sdk/sdk.yaml | 16 +++ .../getting-started/app-vue.snippet.md | 31 ++++ .../getting-started/create-vue.snippet.md | 15 ++ .../getting-started/install.snippet.md | 15 ++ .../getting-started/main-js.snippet.md | 30 ++++ 140 files changed, 3922 insertions(+) create mode 100644 snippets/sdks/android-client-sdk/sdk.yaml create mode 100644 snippets/sdks/android-client-sdk/snippets/getting-started/activity-main-xml.snippet.md create mode 100644 snippets/sdks/android-client-sdk/snippets/getting-started/build-gradle.snippet.md create mode 100644 snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md create mode 100644 snippets/sdks/android-client-sdk/snippets/getting-started/main-application.snippet.md create mode 100644 snippets/sdks/android-client-sdk/snippets/getting-started/manifest.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/sdk.yaml create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/build-mkdir.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/clone-sdk.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-build.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-make.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-msvc.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-ninja.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/cmakelists.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/cpp-client-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/sdk.yaml create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/build-mkdir.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/clone-sdk.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-build.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-make.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-msvc.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-ninja.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/cmakelists.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/cpp-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/dotnet-client-sdk/sdk.yaml create mode 100644 snippets/sdks/dotnet-client-sdk/snippets/getting-started/dotnet-new.snippet.md create mode 100644 snippets/sdks/dotnet-client-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/dotnet-client-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md create mode 100644 snippets/sdks/dotnet-client-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/dotnet-server-sdk/sdk.yaml create mode 100644 snippets/sdks/dotnet-server-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/dotnet-server-sdk/snippets/getting-started/program-cs.snippet.md create mode 100644 snippets/sdks/dotnet-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/sdk.yaml create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/app-src.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar-config.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar3-new.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/run-call.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/run-shell.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md create mode 100644 snippets/sdks/erlang-server-sdk/snippets/getting-started/sup-childspecs.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/sdk.yaml create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/cd-into.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/flutter-create.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/min-sdk.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/podfile-platform.snippet.md create mode 100644 snippets/sdks/flutter-client-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/go-server-sdk/sdk.yaml create mode 100644 snippets/sdks/go-server-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/go-server-sdk/snippets/getting-started/main-go.snippet.md create mode 100644 snippets/sdks/go-server-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/go-server-sdk/snippets/getting-started/mod-init.snippet.md create mode 100644 snippets/sdks/go-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/haskell-server-sdk/sdk.yaml create mode 100644 snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md create mode 100644 snippets/sdks/haskell-server-sdk/snippets/getting-started/package-yaml.snippet.md create mode 100644 snippets/sdks/haskell-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-new.snippet.md create mode 100644 snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-yaml.snippet.md create mode 100644 snippets/sdks/ios-client-sdk/sdk.yaml create mode 100644 snippets/sdks/ios-client-sdk/snippets/getting-started/app-delegate.snippet.md create mode 100644 snippets/sdks/ios-client-sdk/snippets/getting-started/pod-install.snippet.md create mode 100644 snippets/sdks/ios-client-sdk/snippets/getting-started/podfile.snippet.md create mode 100644 snippets/sdks/ios-client-sdk/snippets/getting-started/view-controller.snippet.md create mode 100644 snippets/sdks/java-server-sdk/sdk.yaml create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/app-java.snippet.md create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/cd-into.snippet.md create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/mvn-generate.snippet.md create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/pom-build.snippet.md create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/pom-compiler.snippet.md create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/pom-dependency.snippet.md create mode 100644 snippets/sdks/java-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/js-client-sdk/sdk.yaml create mode 100644 snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md create mode 100644 snippets/sdks/lua-server-sdk/sdk.yaml create mode 100644 snippets/sdks/lua-server-sdk/snippets/getting-started/cpp-build.snippet.md create mode 100644 snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md create mode 100644 snippets/sdks/lua-server-sdk/snippets/getting-started/luarocks-install.snippet.md create mode 100644 snippets/sdks/lua-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/node-client-sdk/sdk.yaml create mode 100644 snippets/sdks/node-client-sdk/snippets/getting-started/index-js.snippet.md create mode 100644 snippets/sdks/node-client-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/node-client-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/node-client-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/node-server-sdk/sdk.yaml create mode 100644 snippets/sdks/node-server-sdk/snippets/getting-started/index-js.snippet.md create mode 100644 snippets/sdks/node-server-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/node-server-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/node-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/php-server-sdk/sdk.yaml create mode 100644 snippets/sdks/php-server-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/php-server-sdk/snippets/getting-started/main-php.snippet.md create mode 100644 snippets/sdks/php-server-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/php-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/react-client-sdk/sdk.yaml create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/create-vite.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/legacy-create.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/legacy-index-tsx.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/legacy-install.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/legacy-run.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md create mode 100644 snippets/sdks/react-client-sdk/snippets/getting-started/run-dev.snippet.md create mode 100644 snippets/sdks/react-native-client-sdk/sdk.yaml create mode 100644 snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md create mode 100644 snippets/sdks/react-native-client-sdk/snippets/getting-started/create-expo.snippet.md create mode 100644 snippets/sdks/react-native-client-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/react-native-client-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/react-native-client-sdk/snippets/getting-started/welcome-tsx.snippet.md create mode 100644 snippets/sdks/roku-client-sdk/sdk.yaml create mode 100644 snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-brs.snippet.md create mode 100644 snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-xml.snippet.md create mode 100644 snippets/sdks/roku-client-sdk/snippets/getting-started/main-brs.snippet.md create mode 100644 snippets/sdks/roku-client-sdk/snippets/getting-started/manifest.snippet.md create mode 100644 snippets/sdks/roku-client-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/ruby-server-sdk/sdk.yaml create mode 100644 snippets/sdks/ruby-server-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/ruby-server-sdk/snippets/getting-started/main-rb.snippet.md create mode 100644 snippets/sdks/ruby-server-sdk/snippets/getting-started/mkdir.snippet.md create mode 100644 snippets/sdks/ruby-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/rust-server-sdk/sdk.yaml create mode 100644 snippets/sdks/rust-server-sdk/snippets/getting-started/cargo-new.snippet.md create mode 100644 snippets/sdks/rust-server-sdk/snippets/getting-started/install-sdk.snippet.md create mode 100644 snippets/sdks/rust-server-sdk/snippets/getting-started/install-tokio.snippet.md create mode 100644 snippets/sdks/rust-server-sdk/snippets/getting-started/main-rs.snippet.md create mode 100644 snippets/sdks/rust-server-sdk/snippets/getting-started/run.snippet.md create mode 100644 snippets/sdks/vue-client-sdk/sdk.yaml create mode 100644 snippets/sdks/vue-client-sdk/snippets/getting-started/app-vue.snippet.md create mode 100644 snippets/sdks/vue-client-sdk/snippets/getting-started/create-vue.snippet.md create mode 100644 snippets/sdks/vue-client-sdk/snippets/getting-started/install.snippet.md create mode 100644 snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md diff --git a/snippets/sdks/android-client-sdk/sdk.yaml b/snippets/sdks/android-client-sdk/sdk.yaml new file mode 100644 index 0000000..c277670 --- /dev/null +++ b/snippets/sdks/android-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: android-client-sdk +sdk-meta-id: android +display-name: Android +type: client-side +languages: + - id: kotlin + extensions: [".kt"] +package-managers: [gradle] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/android.tsx +docs: + reference-page: /sdk/client-side/android +hello-world-repo: launchdarkly/hello-android diff --git a/snippets/sdks/android-client-sdk/snippets/getting-started/activity-main-xml.snippet.md b/snippets/sdks/android-client-sdk/snippets/getting-started/activity-main-xml.snippet.md new file mode 100644 index 0000000..e1ecbbf --- /dev/null +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/activity-main-xml.snippet.md @@ -0,0 +1,19 @@ +--- +id: android-client-sdk/getting-started/activity-main-xml +sdk: android-client-sdk +kind: manifest-fragment +lang: xml +description: TextView fragment to add to layout/activity_main.xml. +ld-application: + slot: activity-main-xml +--- + +Add a `TextView` to your `layout/activity_main.xml` with id `textview`: + +```xml + +``` diff --git a/snippets/sdks/android-client-sdk/snippets/getting-started/build-gradle.snippet.md b/snippets/sdks/android-client-sdk/snippets/getting-started/build-gradle.snippet.md new file mode 100644 index 0000000..932da6f --- /dev/null +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/build-gradle.snippet.md @@ -0,0 +1,23 @@ +--- +id: android-client-sdk/getting-started/build-gradle +sdk: android-client-sdk +kind: manifest-fragment +lang: gradle +description: Gradle dependency entry to drop into app/build.gradle. +inputs: + version: + type: string + description: SDK version. Defaults to '5.0.0' in gonfalon as a fallback when the async fetch hasn't completed. + runtime-default: "5.0.0" +ld-application: + slot: build-gradle +--- + +Add the LaunchDarkly SDK as a dependency in the `app/build.gradle` file: + +```gradle +dependencies { + ... + implementation("com.launchdarkly:launchdarkly-android-client-sdk:{{ version }}") +} +``` diff --git a/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md b/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md new file mode 100644 index 0000000..2f3852c --- /dev/null +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md @@ -0,0 +1,82 @@ +--- +id: android-client-sdk/getting-started/main-activity +sdk: android-client-sdk +kind: hello-world +lang: kotlin +file: app/src/main/java/com/launchdarkly/hello_android/MainActivity.kt +description: MainActivity that observes the flag and renders the value. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: main-activity +# Validator pending — Android validation needs setup-android + a Linux +# emulator boot, slow but feasible. Deferred. +--- + +Open the file `MainActivity.kt` and add the following code: + +```kotlin +package com.launchdarkly.hello_android + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.launchdarkly.hello_android.MainApplication.Companion.LAUNCHDARKLY_MOBILE_KEY +import com.launchdarkly.sdk.android.LDClient + +class MainActivity : AppCompatActivity() { + + // Set BOOLEAN_FLAG_KEY to the feature flag key you want to evaluate. + val BOOLEAN_FLAG_KEY = "{{ featureKey }}" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val textView : TextView = findViewById(R.id.textview) + val fullView : View = window.decorView + + if (LAUNCHDARKLY_MOBILE_KEY == "example-mobile-key") { + val builder = AlertDialog.Builder(this) + builder.setMessage("LAUNCHDARKLY_MOBILE_KEY was not customized for this application.") + builder.create().show() + } + + val client = LDClient.get() + val flagValue = client.boolVariation(BOOLEAN_FLAG_KEY, false) + + // to get the variation the SDK has cached + textView.text = getString( + R.string.flag_evaluated, + BOOLEAN_FLAG_KEY, + flagValue.toString() + ) + + // Style the display + textView.setTextColor(resources.getColor(R.color.colorText)) + if(flagValue) { + fullView.setBackgroundColor(resources.getColor(R.color.colorBackgroundTrue)) + } else { + fullView.setBackgroundColor(resources.getColor(R.color.colorBackgroundFalse)) + } + + // to register a listener to get updates in real time + client.registerFeatureFlagListener(BOOLEAN_FLAG_KEY) { + val changedFlagValue = client.boolVariation(BOOLEAN_FLAG_KEY, false) + textView.text = getString( + R.string.flag_evaluated, + BOOLEAN_FLAG_KEY, + changedFlagValue.toString() + ) + if(changedFlagValue) { + fullView.setBackgroundColor(resources.getColor(R.color.colorBackgroundTrue)) + } else { + fullView.setBackgroundColor(resources.getColor(R.color.colorBackgroundFalse)) + } + } + } +} +``` diff --git a/snippets/sdks/android-client-sdk/snippets/getting-started/main-application.snippet.md b/snippets/sdks/android-client-sdk/snippets/getting-started/main-application.snippet.md new file mode 100644 index 0000000..e43508e --- /dev/null +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/main-application.snippet.md @@ -0,0 +1,69 @@ +--- +id: android-client-sdk/getting-started/main-application +sdk: android-client-sdk +kind: hello-world +lang: kotlin +file: app/src/main/java/com/launchdarkly/hello_android/MainApplication.kt +description: Application subclass that initializes the LDClient on app start. +inputs: + mobileKey: + type: mobile-key + description: Mobile key baked into the rendered source. +ld-application: + slot: main-application +--- + +Create `MainApplication.kt` and add the following code: + +```kotlin +package com.launchdarkly.hello_android + +import android.app.Application +import com.launchdarkly.sdk.ContextKind +import com.launchdarkly.sdk.LDContext +import com.launchdarkly.sdk.android.LDClient +import com.launchdarkly.sdk.android.LDConfig +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes + +class MainApplication : Application() { + + companion object { + + // Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly SDK mobile key. + const val LAUNCHDARKLY_MOBILE_KEY = "{{ mobileKey }}" + } + + override fun onCreate() { + super.onCreate() + + // Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly mobile key found on the LaunchDarkly + // dashboard in the start guide. + // If you want to disable the Auto EnvironmentAttributes functionality. + // Use AutoEnvAttributes.Disabled as the argument to the Builder + val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled) + .mobileKey(LAUNCHDARKLY_MOBILE_KEY) + .build() + + // Set up the context properties. This context should appear on your LaunchDarkly context + // dashboard soon after you run the demo. + val context = if (isUserLoggedIn()) { + LDContext.builder(ContextKind.DEFAULT, getUserKey()) + .name(getUserName()) + .build() + } else { + LDContext.builder(ContextKind.DEFAULT, "example-user-key") + .anonymous(true) + .build() + } + + LDClient.init(this@MainApplication, ldConfig, context) + } + + private fun isUserLoggedIn(): Boolean = false + + private fun getUserKey(): String = "example-user-key" + + private fun getUserName(): String = "Sandy" + +} +``` diff --git a/snippets/sdks/android-client-sdk/snippets/getting-started/manifest.snippet.md b/snippets/sdks/android-client-sdk/snippets/getting-started/manifest.snippet.md new file mode 100644 index 0000000..3c9b82e --- /dev/null +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/manifest.snippet.md @@ -0,0 +1,23 @@ +--- +id: android-client-sdk/getting-started/manifest +sdk: android-client-sdk +kind: manifest-fragment +lang: xml +description: AndroidManifest.xml fragment registering MainApplication. +ld-application: + slot: manifest +--- + +Register the `MainApplication` class in the `AndroidManifest.xml`: + +```xml + + + + +``` diff --git a/snippets/sdks/cpp-client-sdk/sdk.yaml b/snippets/sdks/cpp-client-sdk/sdk.yaml new file mode 100644 index 0000000..cd966cc --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: cpp-client-sdk +sdk-meta-id: cpp-client +display-name: C++ (client-side) +type: client-side +languages: + - id: cpp + extensions: [".cpp", ".h", ".hpp"] +package-managers: [cmake] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/cppClient.tsx +docs: + reference-page: /sdk/client-side/c-c-- +hello-world-repo: launchdarkly/cpp-sdks diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/build-mkdir.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/build-mkdir.snippet.md new file mode 100644 index 0000000..e9bb45d --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/build-mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-client-sdk/getting-started/build-mkdir +sdk: cpp-client-sdk +kind: bootstrap +lang: bash +description: Create the CMake build directory. +ld-application: + slot: build-mkdir +--- + +Create a build directory: + +```bash +mkdir build && cd build +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/clone-sdk.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/clone-sdk.snippet.md new file mode 100644 index 0000000..7486068 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/clone-sdk.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-client-sdk/getting-started/clone-sdk +sdk: cpp-client-sdk +kind: install +lang: bash +description: Clone the C++ SDK repo into the project directory. +ld-application: + slot: clone-sdk +--- + +Clone the C++ SDK inside the directory you created above using git: + +```bash +git clone https://github.com/launchdarkly/cpp-sdks.git +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-build.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-build.snippet.md new file mode 100644 index 0000000..e247333 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-build.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-client-sdk/getting-started/cmake-build +sdk: cpp-client-sdk +kind: install +lang: bash +description: Build the SDK and project. +ld-application: + slot: cmake-build +--- + +Build the SDK: + +```bash +cmake --build . +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-make.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-make.snippet.md new file mode 100644 index 0000000..93bed58 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-make.snippet.md @@ -0,0 +1,13 @@ +--- +id: cpp-client-sdk/getting-started/cmake-make +sdk: cpp-client-sdk +kind: bootstrap +lang: bash +description: Configure with the Make generator. +ld-application: + slot: cmake-make +--- + +```bash +cmake -G"Unix Makefiles" -DBUILD_TESTING=OFF .. +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-msvc.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-msvc.snippet.md new file mode 100644 index 0000000..9cfb204 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-msvc.snippet.md @@ -0,0 +1,13 @@ +--- +id: cpp-client-sdk/getting-started/cmake-msvc +sdk: cpp-client-sdk +kind: bootstrap +lang: bash +description: Configure with Visual Studio 2022. +ld-application: + slot: cmake-msvc +--- + +```bash +cmake -G"Visual Studio 17 2022" -DBUILD_TESTING=OFF .. +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-ninja.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-ninja.snippet.md new file mode 100644 index 0000000..0438afa --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmake-ninja.snippet.md @@ -0,0 +1,13 @@ +--- +id: cpp-client-sdk/getting-started/cmake-ninja +sdk: cpp-client-sdk +kind: bootstrap +lang: bash +description: Configure with the Ninja generator. +ld-application: + slot: cmake-ninja +--- + +```bash +cmake -G"Ninja" -DBUILD_TESTING=OFF .. +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmakelists.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmakelists.snippet.md new file mode 100644 index 0000000..4581b91 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/cmakelists.snippet.md @@ -0,0 +1,37 @@ +--- +id: cpp-client-sdk/getting-started/cmakelists +sdk: cpp-client-sdk +kind: manifest +lang: text +file: CMakeLists.txt +description: CMake configuration for the hello-cpp-client project. +ld-application: + slot: cmakelists +--- + +Create a `CMakeLists.txt` file with the following content: + +```text +cmake_minimum_required(VERSION 3.19) + +project( + CPPClientQuickstart + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP Client-side SDK Quickstart" + LANGUAGES CXX +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_subdirectory(cpp-sdks) + +add_executable(cpp-client-quickstart main.cpp) + +target_link_libraries(cpp-client-quickstart + PRIVATE + launchdarkly::client + Threads::Threads +) + +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md new file mode 100644 index 0000000..0b74fe5 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md @@ -0,0 +1,73 @@ +--- +id: cpp-client-sdk/getting-started/main-cpp +sdk: cpp-client-sdk +kind: hello-world +lang: cpp +file: main.cpp +description: Hello-world program that initializes the C++ client SDK and evaluates a feature flag. +inputs: + mobileKey: + type: mobile-key + description: Mobile key baked into the rendered source. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: main-cpp +# Validator pending — same toolchain story as cpp-server-sdk. +--- + +Create a file named `main.cpp` add the following code: + +```cpp +#include +#include + +#include +#include + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +using namespace launchdarkly; +using namespace launchdarkly::client_side; + +int main() { + + auto config = ConfigBuilder("{{ mobileKey }}").Build(); + if (!config) { + std::cout << "error: config is invalid: " << config.error() << std::endl; + return 1; + } + + auto context = + ContextBuilder().Kind("user", "example-user-key").Name("Sandy").Build(); + + auto client = Client(std::move(*config), std::move(context)); + + auto start_result = client.StartAsync(); + + if (auto const status = start_result.wait_for( + std::chrono::milliseconds(INIT_TIMEOUT_MILLISECONDS)); + status == std::future_status::ready) { + if (start_result.get()) { + std::cout << "*** SDK successfully initialized!" << std::endl; + } else { + std::cout << "*** SDK failed to initialize" << std::endl; + return 1; + } + } else { + std::cout << "*** SDK initialization didn't complete in " + << INIT_TIMEOUT_MILLISECONDS << "ms" << std::endl; + return 1; + } + + bool const flag_value = client.BoolVariation("{{ featureKey }}", false); + + std::cout << "*** Feature flag '{{ featureKey }}' is " + << (flag_value ? "true" : "false") << std::endl; + + return 0; +} +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..1a404b0 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-client-sdk/getting-started/mkdir +sdk: cpp-client-sdk +kind: bootstrap +lang: bash +description: Create the project directory. +ld-application: + slot: mkdir +--- + +Create a new project directory: + +```bash +mkdir hello-cpp-client && cd hello-cpp-client +``` diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..f12c72f --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-client-sdk/getting-started/run +sdk: cpp-client-sdk +kind: run +lang: bash +description: Run the built binary. +ld-application: + slot: run +--- + +Run: + +```bash +./cpp-client-quickstart +``` diff --git a/snippets/sdks/cpp-server-sdk/sdk.yaml b/snippets/sdks/cpp-server-sdk/sdk.yaml new file mode 100644 index 0000000..86710bd --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: cpp-server-sdk +sdk-meta-id: cpp-server +display-name: C++ (server-side) +type: server-side +languages: + - id: cpp + extensions: [".cpp", ".h", ".hpp"] +package-managers: [cmake] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/cppServer.tsx +docs: + reference-page: /sdk/server-side/c-c-- +hello-world-repo: launchdarkly/cpp-sdks diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/build-mkdir.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/build-mkdir.snippet.md new file mode 100644 index 0000000..fe98206 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/build-mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-server-sdk/getting-started/build-mkdir +sdk: cpp-server-sdk +kind: bootstrap +lang: bash +description: Create the CMake build directory. +ld-application: + slot: build-mkdir +--- + +Create a build directory: + +```bash +mkdir build && cd build +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/clone-sdk.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/clone-sdk.snippet.md new file mode 100644 index 0000000..5b3ccb9 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/clone-sdk.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-server-sdk/getting-started/clone-sdk +sdk: cpp-server-sdk +kind: install +lang: shell +description: Clone the C++ SDK repo into the project directory. +ld-application: + slot: clone-sdk +--- + +Clone the C++ SDK inside the directory you created above using git: + +```shell +git clone https://github.com/launchdarkly/cpp-sdks.git +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-build.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-build.snippet.md new file mode 100644 index 0000000..2814c8c --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-build.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-server-sdk/getting-started/cmake-build +sdk: cpp-server-sdk +kind: install +lang: bash +description: Build the SDK and project. +ld-application: + slot: cmake-build +--- + +Build the SDK: + +```bash +cmake --build . +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-make.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-make.snippet.md new file mode 100644 index 0000000..f800ffd --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-make.snippet.md @@ -0,0 +1,13 @@ +--- +id: cpp-server-sdk/getting-started/cmake-make +sdk: cpp-server-sdk +kind: bootstrap +lang: bash +description: Configure with the Make generator. +ld-application: + slot: cmake-make +--- + +```bash +cmake -G"Unix Makefiles" -DBUILD_TESTING=OFF .. +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-msvc.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-msvc.snippet.md new file mode 100644 index 0000000..1d58e62 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-msvc.snippet.md @@ -0,0 +1,13 @@ +--- +id: cpp-server-sdk/getting-started/cmake-msvc +sdk: cpp-server-sdk +kind: bootstrap +lang: bash +description: Configure with Visual Studio 2022. +ld-application: + slot: cmake-msvc +--- + +```bash +cmake -G"Visual Studio 17 2022" -DBUILD_TESTING=OFF .. +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-ninja.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-ninja.snippet.md new file mode 100644 index 0000000..28919f1 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmake-ninja.snippet.md @@ -0,0 +1,13 @@ +--- +id: cpp-server-sdk/getting-started/cmake-ninja +sdk: cpp-server-sdk +kind: bootstrap +lang: bash +description: Configure with the Ninja generator. +ld-application: + slot: cmake-ninja +--- + +```bash +cmake -G"Ninja" -DBUILD_TESTING=OFF .. +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmakelists.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmakelists.snippet.md new file mode 100644 index 0000000..7a48887 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/cmakelists.snippet.md @@ -0,0 +1,37 @@ +--- +id: cpp-server-sdk/getting-started/cmakelists +sdk: cpp-server-sdk +kind: manifest +lang: cpp +file: CMakeLists.txt +description: CMake configuration file for the hello-cpp-server project. +ld-application: + slot: cmakelists +--- + +Create a `CMakeLists.txt` file with the following content: + +```cpp +cmake_minimum_required(VERSION 3.19) + +project( + CPPServerQuickstart + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP Server-side SDK Quickstart" + LANGUAGES CXX +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_subdirectory(cpp-sdks) + +add_executable(cpp-server-quickstart main.cpp) + +target_link_libraries(cpp-server-quickstart + PRIVATE + launchdarkly::server + Threads::Threads +) + +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md new file mode 100644 index 0000000..dd9b128 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md @@ -0,0 +1,76 @@ +--- +id: cpp-server-sdk/getting-started/main-cpp +sdk: cpp-server-sdk +kind: hello-world +lang: cpp +file: main.cpp +description: Hello-world program that initializes the C++ server SDK and evaluates a feature flag. +inputs: + apiKey: + type: sdk-key + description: SDK key baked into the rendered source. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: main-cpp +# Validator pending. Per-validate cycle requires a Docker image with +# cmake + boost + openssl + ninja and a checkout of cpp-sdks; first +# build is multi-minute even with prebuilt deps. +--- + +Create a file named `main.cpp` add the following code: + +```cpp +#include +#include +#include + +#include +#include + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +int main() { + auto config = ConfigBuilder("{{ apiKey }}").Build(); + if (!config) { + std::cout << "error: config is invalid: " << config.error() << std::endl; + return 1; + } + + auto client = Client(std::move(*config)); + + auto start_result = client.StartAsync(); + + if (auto const status = start_result.wait_for( + std::chrono::milliseconds(INIT_TIMEOUT_MILLISECONDS)); + status == std::future_status::ready) { + if (start_result.get()) { + std::cout << "*** SDK successfully initialized!" << std::endl; + } else { + std::cout << "*** SDK failed to initialize" << std::endl; + return 1; + } + } else { + std::cout << "*** SDK initialization didn't complete in " + << INIT_TIMEOUT_MILLISECONDS << "ms" << std::endl; + return 1; + } + + auto const context = + ContextBuilder().Kind("user", "example-user-key").Name("Sandy").Build(); + + bool const flag_value = + client.BoolVariation(context, "{{ featureKey }}", false); + + std::cout << "*** Feature flag '{{ featureKey }}' is " + << (flag_value ? "true" : "false") << std::endl; + + return 0; +} +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..55463d9 --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: cpp-server-sdk/getting-started/mkdir +sdk: cpp-server-sdk +kind: bootstrap +lang: shell +description: Create the project directory. +ld-application: + slot: mkdir +--- + +Create a new project directory: + +```shell +mkdir hello-cpp-server && cd hello-cpp-server +``` diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..899d43b --- /dev/null +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,16 @@ +--- +id: cpp-server-sdk/getting-started/run +sdk: cpp-server-sdk +kind: run +lang: bash +description: Run the built binary. +ld-application: + slot: run +--- + +Run: + +```bash +./cpp-server-quickstart + +``` diff --git a/snippets/sdks/dotnet-client-sdk/sdk.yaml b/snippets/sdks/dotnet-client-sdk/sdk.yaml new file mode 100644 index 0000000..850af30 --- /dev/null +++ b/snippets/sdks/dotnet-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: dotnet-client-sdk +sdk-meta-id: dotnet-client +display-name: .NET (client-side) +type: client-side +languages: + - id: csharp + extensions: [".cs"] +package-managers: [nuget] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/dotnetClient.tsx +docs: + reference-page: /sdk/client-side/dotnet +hello-world-repo: launchdarkly/hello-dotnet-client diff --git a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/dotnet-new.snippet.md b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/dotnet-new.snippet.md new file mode 100644 index 0000000..ad8bbd2 --- /dev/null +++ b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/dotnet-new.snippet.md @@ -0,0 +1,15 @@ +--- +id: dotnet-client-sdk/getting-started/dotnet-new +sdk: dotnet-client-sdk +kind: bootstrap +lang: shell +description: Create a new .NET console application. +ld-application: + slot: dotnet-new +--- + +Next, create a new console application: + +```shell +dotnet new console +``` diff --git a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..d5aa58f --- /dev/null +++ b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: dotnet-client-sdk/getting-started/install +sdk: dotnet-client-sdk +kind: install +lang: shell +description: Add the LaunchDarkly client SDK as a dependency. +ld-application: + slot: install +--- + +Next, add the LaunchDarkly dependency to the project: + +```shell +dotnet add package Launchdarkly.ClientSdk +``` diff --git a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..1eff91c --- /dev/null +++ b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,16 @@ +--- +id: dotnet-client-sdk/getting-started/mkdir +sdk: dotnet-client-sdk +kind: bootstrap +lang: shell +description: Create the project directory. +ld-application: + slot: mkdir +--- + +Create a new folder for your project: + +```shell +mkdir HelloDotNetClient +cd HelloDotNetClient +``` diff --git a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md new file mode 100644 index 0000000..38900b6 --- /dev/null +++ b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md @@ -0,0 +1,56 @@ +--- +id: dotnet-client-sdk/getting-started/program-cs +sdk: dotnet-client-sdk +kind: hello-world +lang: csharp +file: Program.cs +description: Hello-world program that initializes the .NET client SDK and evaluates a feature flag. +inputs: + mobileKey: + type: mobile-key + description: Mobile key baked into the rendered source. Validation reads LAUNCHDARKLY_MOBILE_KEY at runtime. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: program-cs +validation: + runtime: dotnet-client + requirements: Launchdarkly.ClientSdk +--- + +Open the file `Program.cs` and add the following code: + +```csharp +using LaunchDarkly.Sdk; +using LaunchDarkly.Sdk.Client; + +var context = Context.New("context-key-123abc"); +var timeSpan = TimeSpan.FromSeconds(10); +var client = LdClient.Init( + Configuration.Default("{{ mobileKey }}", ConfigurationBuilder.AutoEnvAttributes.Enabled), + context, + timeSpan +); + +if (client.Initialized) +{ + Console.WriteLine("SDK successfully initialized!"); +} +else +{ + Console.WriteLine("SDK failed to initialize"); + Environment.Exit(1); +} + +var flagValue = client.BoolVariation("{{ featureKey }}", false); + +Console.WriteLine(string.Format("The '{{ featureKey }}' feature flag evaluates to {0}.", flagValue)); + +// Here we ensure that the SDK shuts down cleanly and has a chance to deliver analytics +// events to LaunchDarkly before the program exits. If analytics events are not delivered, +// the context properties and flag usage statistics will not appear on your dashboard. In +// a normal long-running application, the SDK would continue running and events would be +// delivered automatically in the background. +client.Dispose(); +``` diff --git a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..e00f1a4 --- /dev/null +++ b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,15 @@ +--- +id: dotnet-client-sdk/getting-started/run +sdk: dotnet-client-sdk +kind: run +lang: shell +description: Run the program. +ld-application: + slot: run +--- + +Use the following command to run the code: + +```shell +dotnet run +``` diff --git a/snippets/sdks/dotnet-server-sdk/sdk.yaml b/snippets/sdks/dotnet-server-sdk/sdk.yaml new file mode 100644 index 0000000..46cf7ca --- /dev/null +++ b/snippets/sdks/dotnet-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: dotnet-server-sdk +sdk-meta-id: dotnet-server +display-name: .NET (server-side) +type: server-side +languages: + - id: csharp + extensions: [".cs"] +package-managers: [nuget] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/dotnet.tsx +docs: + reference-page: /sdk/server-side/dotnet +hello-world-repo: launchdarkly/hello-dotnet-server diff --git a/snippets/sdks/dotnet-server-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/dotnet-server-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..bb14a5b --- /dev/null +++ b/snippets/sdks/dotnet-server-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: dotnet-server-sdk/getting-started/install +sdk: dotnet-server-sdk +kind: install +lang: powershell +description: Install the LaunchDarkly server SDK via NuGet. +ld-application: + slot: install +--- + +Next, install the LaunchDarkly SDK using [NuGet](http://docs.nuget.org/docs/start-here/using-the-package-manager-console): + +```powershell +Install-Package LaunchDarkly.ServerSdk +``` diff --git a/snippets/sdks/dotnet-server-sdk/snippets/getting-started/program-cs.snippet.md b/snippets/sdks/dotnet-server-sdk/snippets/getting-started/program-cs.snippet.md new file mode 100644 index 0000000..c5e7fa1 --- /dev/null +++ b/snippets/sdks/dotnet-server-sdk/snippets/getting-started/program-cs.snippet.md @@ -0,0 +1,115 @@ +--- +id: dotnet-server-sdk/getting-started/program-cs +sdk: dotnet-server-sdk +kind: hello-world +lang: csharp +file: Program.cs +description: Hello-world program that initializes the .NET server SDK and watches a feature flag. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: program-cs +validation: + runtime: dotnet-server + requirements: LaunchDarkly.ServerSdk +--- + +Open the file `Program.cs` and add the following code: + +```csharp +using System; + using System.Threading.Tasks; + using LaunchDarkly.Sdk; + using LaunchDarkly.Sdk.Server; + + namespace HelloDotNet + { + class Hello + { + public static void ShowBanner(){ + Console.WriteLine( + @" ██ + ██ + ████████ + ███████ + ██ LAUNCHDARKLY █ + ███████ + ████████ + ██ + ██ + "); + } + + static void Main(string[] args) + { + bool CI = Environment.GetEnvironmentVariable("CI") != null; + + string SdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY"); + + // Set FeatureFlagKey to the feature flag key you want to evaluate. + string FeatureFlagKey = "{{ featureKey }}"; + + if (string.IsNullOrEmpty(SdkKey)) + { + Console.WriteLine("*** Please set LAUNCHDARKLY_SDK_KEY environment variable to your LaunchDarkly SDK key first\n"); + Environment.Exit(1); + } + + var ldConfig = Configuration.Default(SdkKey); + + var client = new LdClient(ldConfig); + + if (client.Initialized) + { + Console.WriteLine("*** SDK successfully initialized!\n"); + } + else + { + Console.WriteLine("*** SDK failed to initialize\n"); + Environment.Exit(1); + } + + // Set up the evaluation context. This context should appear on your LaunchDarkly contexts + // dashboard soon after you run the demo. + var context = Context.Builder("example-user-key") + .Name("Sandy") + .Build(); + + if (Environment.GetEnvironmentVariable("LAUNCHDARKLY_FLAG_KEY") != null) + { + FeatureFlagKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_FLAG_KEY"); + } + + var flagValue = client.BoolVariation(FeatureFlagKey, context, false); + + Console.WriteLine(string.Format("*** The {0} feature flag evaluates to {1}.\n", + FeatureFlagKey, flagValue)); + + if (flagValue) + { + ShowBanner(); + } + + client.FlagTracker.FlagChanged += client.FlagTracker.FlagValueChangeHandler( + FeatureFlagKey, + context, + (sender, changeArgs) => { + Console.WriteLine(string.Format("*** The {0} feature flag evaluates to {1}.\n", + FeatureFlagKey, changeArgs.NewValue)); + + if (changeArgs.NewValue.AsBool) ShowBanner(); + } + ); + + if(CI) Environment.Exit(0); + + Console.WriteLine("*** Waiting for changes \n"); + + Task waitForever = new Task(() => {}); + waitForever.Wait(); + } + } + } +``` diff --git a/snippets/sdks/dotnet-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/dotnet-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..a9fe6f9 --- /dev/null +++ b/snippets/sdks/dotnet-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: dotnet-server-sdk/getting-started/run +sdk: dotnet-server-sdk +kind: run +lang: shell +description: Run with dotnet from the command line. +inputs: + apiKey: + type: sdk-key + description: SDK key embedded in the rendered Run command. +ld-application: + slot: run +--- + +Run from the command line: + +```shell +LAUNCHDARKLY_SDK_KEY={{ apiKey }} dotnet run --project HelloDotNet +``` diff --git a/snippets/sdks/erlang-server-sdk/sdk.yaml b/snippets/sdks/erlang-server-sdk/sdk.yaml new file mode 100644 index 0000000..cf2ab95 --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: erlang-server-sdk +sdk-meta-id: erlang +display-name: Erlang +type: server-side +languages: + - id: erlang + extensions: [".erl"] +package-managers: [rebar3] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/erlang.tsx +docs: + reference-page: /sdk/server-side/erlang +hello-world-repo: launchdarkly/hello-erlang diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/app-src.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/app-src.snippet.md new file mode 100644 index 0000000..a52a673 --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/app-src.snippet.md @@ -0,0 +1,19 @@ +--- +id: erlang-server-sdk/getting-started/app-src +sdk: erlang-server-sdk +kind: manifest-fragment +lang: erlang +description: applications block to add to src/hello_erlang.app.src. +ld-application: + slot: app-src +--- + +Edit `src/hello_erlang.app.src` to import LaunchDarkly: + +```erlang +{applications, + [kernel, + stdlib, + ldclient +]}, +``` diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar-config.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar-config.snippet.md new file mode 100644 index 0000000..84262f5 --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar-config.snippet.md @@ -0,0 +1,20 @@ +--- +id: erlang-server-sdk/getting-started/rebar-config +sdk: erlang-server-sdk +kind: manifest-fragment +lang: erlang +description: rebar.config dependency entry for the LaunchDarkly Erlang server SDK. +inputs: + version: + type: string + description: SDK version. Gonfalon fetches the latest from Hex asynchronously. + runtime-default: "" +ld-application: + slot: rebar-config +--- + +Next, add the SDK package to your list of dependencies in `rebar.config`: + +```erlang +{ldclient, "{{ version }}", {pkg, launchdarkly_server_sdk}} +``` diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar3-new.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar3-new.snippet.md new file mode 100644 index 0000000..e11a41e --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/rebar3-new.snippet.md @@ -0,0 +1,15 @@ +--- +id: erlang-server-sdk/getting-started/rebar3-new +sdk: erlang-server-sdk +kind: bootstrap +lang: shell +description: Bootstrap a rebar3 application. +ld-application: + slot: rebar3-new +--- + +Create a new project for your application: + +```shell +rebar3 new app hello_erlang && cd hello_erlang +``` diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/run-call.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/run-call.snippet.md new file mode 100644 index 0000000..c0ff2b7 --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/run-call.snippet.md @@ -0,0 +1,19 @@ +--- +id: erlang-server-sdk/getting-started/run-call +sdk: erlang-server-sdk +kind: run +lang: erlang +description: Erlang shell call to evaluate the flag. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered command. +ld-application: + slot: run-call +--- + +Inside the rebar3 shell, call: + +```erlang +hello_erlang_server:get(<<"{{ featureKey }}">>, "FALLBACK_VALUE", <<"user@example.com">>). +``` diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/run-shell.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/run-shell.snippet.md new file mode 100644 index 0000000..9086693 --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/run-shell.snippet.md @@ -0,0 +1,15 @@ +--- +id: erlang-server-sdk/getting-started/run-shell +sdk: erlang-server-sdk +kind: run +lang: shell +description: Open the rebar3 shell so the gen_server can be queried. +ld-application: + slot: run-shell +--- + +Start the shell: + +```shell +rebar3 shell +``` diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md new file mode 100644 index 0000000..ab5f8fc --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md @@ -0,0 +1,66 @@ +--- +id: erlang-server-sdk/getting-started/server-erl +sdk: erlang-server-sdk +kind: hello-world +lang: erlang +file: src/hello_erlang_server.erl +description: gen_server module that wraps the LaunchDarkly Erlang client. +inputs: + apiKey: + type: sdk-key + description: SDK key baked into the rendered source. Validation is not yet wired — see comment. +ld-application: + slot: server-erl +# Validator pending. The Erlang Get Started flow is fundamentally +# interactive (rebar3 shell + manual gen_server:call). To validate end- +# to-end we'd need to write a wrapper application script that calls +# hello_erlang_server:get/3 and prints an EXAM-HELLO conformant line. +# That's a snippet rewrite and is left as fix-on-red. +--- + +First create a new file named `src/hello_erlang_server.erl`. Then, in +`src/hello_erlang_server.erl` create a new `LDClient` with your *environment-specific* SDK key: + +```erlang +-module(hello_erlang_server). +-behaviour(gen_server). + +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). + +-export([start_link/0]). +-export([get/3]). + +% public functions + +start_link() -> + gen_server:start_link({local, hello_erlang_server}, ?MODULE, [], []). + +get(Key, Fallback, ContextKey) -> gen_server:call(?MODULE, {get, Key, Fallback, ContextKey}). + +% gen_server callbacks + +init(_Args) -> + ldclient:start_instance("{{ apiKey }}", #{ + http_options => #{ + tls_options => ldclient_config:tls_basic_options() + } + }), + {ok, []}. + +handle_call({get, Key, Fallback, ContextKey}, _From, State) -> + Flag = ldclient:variation(Key, ldclient_context:new(ContextKey), Fallback), + {reply, Flag, State}. + +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. +``` diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/sup-childspecs.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/sup-childspecs.snippet.md new file mode 100644 index 0000000..aa78a7b --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/sup-childspecs.snippet.md @@ -0,0 +1,17 @@ +--- +id: erlang-server-sdk/getting-started/sup-childspecs +sdk: erlang-server-sdk +kind: manifest-fragment +lang: erlang +description: ChildSpecs replacement to drop into src/hello_erlang_sup.erl. +ld-application: + slot: sup-childspecs +--- + +Replace the `ChildSpecs` variable in `src/hello_erlang_sup.erl` with the following: + +```erlang +[{console, + {hello_erlang_server, start_link, []}, + permanent, 5000, worker, [hello_erlang_server]}] +``` diff --git a/snippets/sdks/flutter-client-sdk/sdk.yaml b/snippets/sdks/flutter-client-sdk/sdk.yaml new file mode 100644 index 0000000..db5e2c1 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: flutter-client-sdk +sdk-meta-id: flutter +display-name: Flutter +type: client-side +languages: + - id: dart + extensions: [".dart"] +package-managers: [pub] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/flutter.tsx +docs: + reference-page: /sdk/client-side/flutter +hello-world-repo: launchdarkly/hello-flutter diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/cd-into.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/cd-into.snippet.md new file mode 100644 index 0000000..4881325 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/cd-into.snippet.md @@ -0,0 +1,15 @@ +--- +id: flutter-client-sdk/getting-started/cd-into +sdk: flutter-client-sdk +kind: bootstrap +lang: bash +description: Change into the project directory. +ld-application: + slot: cd-into +--- + +Change into the directory of the created project: + +```bash +cd hello_flutter +``` diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/flutter-create.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/flutter-create.snippet.md new file mode 100644 index 0000000..18ca007 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/flutter-create.snippet.md @@ -0,0 +1,15 @@ +--- +id: flutter-client-sdk/getting-started/flutter-create +sdk: flutter-client-sdk +kind: bootstrap +lang: bash +description: Create a new Flutter project for Android and iOS. +ld-application: + slot: flutter-create +--- + +Use the Flutter tool to create a new project: + +```bash +flutter create hello_flutter --platforms android,ios +``` diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..9cdd497 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: flutter-client-sdk/getting-started/install +sdk: flutter-client-sdk +kind: install +lang: bash +description: Add the LaunchDarkly Flutter SDK as a dependency. +ld-application: + slot: install +--- + +Add the LaunchDarkly SDK as a dependency: + +```bash +flutter pub add launchdarkly_flutter_client_sdk +``` diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md new file mode 100644 index 0000000..75d733d --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md @@ -0,0 +1,131 @@ +--- +id: flutter-client-sdk/getting-started/main-dart +sdk: flutter-client-sdk +kind: hello-world +lang: dart +file: lib/main.dart +description: Hello-world Flutter app that initializes the LaunchDarkly SDK and renders the flag value. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: main-dart +# Validator pending — flutter test/run on a Linux runner with +# flutter-action; deferred. +--- + +Open the file `lib/main.dart` and replace with the following code: + +```dart +import 'package:flutter/material.dart'; +import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart'; +import 'package:provider/provider.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + // The LDClient doesn't need to change throughout the lifetime of the + // application, so we wrap the application in a provider with the client. + return Provider( + create: (_) => LDClient( + LDConfig( + // The credentials come from the environment, you can set them + // using --dart-define. + // Examples: + // flutter run --dart-define LAUNCHDARKLY_CLIENT_SIDE_ID= -d Chrome + // flutter run --dart-define LAUNCHDARKLY_MOBILE_KEY= -d ios + // + // Alternatively `CredentialSource.fromEnvironment()` can be replaced with your mobile key. + CredentialSource.fromEnvironment(), + AutoEnvAttributes.enabled, + ), + // Here we are using a default user with key of 'example-user-key'. + LDContextBuilder().kind('user', 'example-user-key') + .setString('name', 'Sandy').build()), + dispose: (_, client) => client.close(), + // We use a future provider to wait for the client to either start, + // or for a timeout to elapse. + child: MaterialApp( + title: 'LaunchDarkly Hello App', + theme: ThemeData( + useMaterial3: true, + ), + home: const MyHomePage(), + )); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +/// Example provider which listens for flag changes and maps them to bool +/// values. It would also be possible to map to some application specific model +/// types. When mapping be sure all values are accessed through the client +/// `variation` methods. This ensures that the SDK generates the expected +/// events. +class FlagProviderBool extends StreamProvider { + FlagProviderBool( + {super.key, + required LDClient client, + required String flagKey, + required bool defaultValue, + required Widget child}) + : super( + create: (context) => client.flagChanges + .where((element) => element.keys.contains(flagKey)) + .map((event) => client.boolVariation(flagKey, defaultValue)), + // Here we get the initial value of the flag. If the SDK is not + // initialized, then the default value will be returned. + initialData: client.boolVariation(flagKey, defaultValue), + child: child); +} + +class _MyHomePageState extends State { + static const String flagKey = '{{ featureKey }}'; + + @override + Widget build(BuildContext context) { + // The FutureBuilder here is used to gate the presentation content + // based on the LaunchDarkly SDK having started. While it has not started, + // a loading indicator will be shown. After it has started, or encountered + // a timeout, then it will render the content. + return FutureBuilder( + future: Provider.of(context, listen: false) + .start() + // In this case we do not have special handling for a failed + // initialization or timeout. + .timeout(const Duration(seconds: 5), onTimeout: () => true) + .then((value) => true), + builder: (context, loaded) => loaded.data ?? false ? + FlagProviderBool( + // The client will not be changing, so we don't need to + // listen for client changes. + client: Provider.of(context, listen: false), + flagKey: flagKey, + defaultValue: false, + child: Consumer( + builder: (context, flagValue, _) => Scaffold( + backgroundColor: flagValue ? const Color(0xFF00844B) : const Color(0xFF373841), + body: + Center( + child: Text( + 'The $flagKey feature flag evaluates to $flagValue', + style: const TextStyle(color: Colors.white, fontSize: 16) + ) + )), + )) : const CircularProgressIndicator()); + } +} +``` diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/min-sdk.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/min-sdk.snippet.md new file mode 100644 index 0000000..6796620 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/min-sdk.snippet.md @@ -0,0 +1,15 @@ +--- +id: flutter-client-sdk/getting-started/min-sdk +sdk: flutter-client-sdk +kind: manifest-fragment +lang: gradle +description: android/app/build.gradle minSdkVersion line. +ld-application: + slot: min-sdk +--- + +Ensure that `android/app/build.gradle` specifies a `minSdkVersion` of at least 21. + +```gradle +minSdkVersion 21 +``` diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/podfile-platform.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/podfile-platform.snippet.md new file mode 100644 index 0000000..dadbe18 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/podfile-platform.snippet.md @@ -0,0 +1,15 @@ +--- +id: flutter-client-sdk/getting-started/podfile-platform +sdk: flutter-client-sdk +kind: manifest-fragment +lang: ruby +description: ios/Podfile platform line. +ld-application: + slot: podfile-platform +--- + +Ensure that `ios/Podfile` specifies a minimum deployment target of at least 10.0. + +```ruby +platform :ios, '10.0' +``` diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..52dc2a5 --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: flutter-client-sdk/getting-started/run +sdk: flutter-client-sdk +kind: run +lang: bash +description: Run the Flutter app with the mobile key passed via --dart-define. +inputs: + mobileKey: + type: mobile-key + description: Mobile key embedded in the rendered Run command. +ld-application: + slot: run +--- + +Run: + +```bash +flutter run --dart-define LAUNCHDARKLY_MOBILE_KEY={{ mobileKey }} +``` diff --git a/snippets/sdks/go-server-sdk/sdk.yaml b/snippets/sdks/go-server-sdk/sdk.yaml new file mode 100644 index 0000000..647cb5b --- /dev/null +++ b/snippets/sdks/go-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: go-server-sdk +sdk-meta-id: go +display-name: Go +type: server-side +languages: + - id: go + extensions: [".go"] +package-managers: [go-modules] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/go.tsx +docs: + reference-page: /sdk/server-side/go +hello-world-repo: launchdarkly/hello-go diff --git a/snippets/sdks/go-server-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/go-server-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..46f7630 --- /dev/null +++ b/snippets/sdks/go-server-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: go-server-sdk/getting-started/install +sdk: go-server-sdk +kind: install +lang: shell +description: Install the Go server SDK module. +ld-application: + slot: install +--- + +Next, install the SDK: + +```shell +go get github.com/launchdarkly/go-server-sdk/v7 +``` diff --git a/snippets/sdks/go-server-sdk/snippets/getting-started/main-go.snippet.md b/snippets/sdks/go-server-sdk/snippets/getting-started/main-go.snippet.md new file mode 100644 index 0000000..eb4dc95 --- /dev/null +++ b/snippets/sdks/go-server-sdk/snippets/getting-started/main-go.snippet.md @@ -0,0 +1,100 @@ +--- +id: go-server-sdk/getting-started/main-go +sdk: go-server-sdk +kind: hello-world +lang: go +file: main.go +description: Hello-world program that initializes the Go server SDK and watches a feature flag. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime; the env var takes precedence. +ld-application: + slot: main-go +validation: + runtime: go +--- + +Create a file called `main.go` and add the following code: + +```go +package main + + import ( + "fmt" + "os" + "time" + + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + ld "github.com/launchdarkly/go-server-sdk/v7" + ) + + func showBanner() { + fmt.Print("\n ██ \n" + + " ██ \n" + + " ████████ \n" + + " ███████ \n" + + "██ LAUNCHDARKLY █\n" + + " ███████ \n" + + " ████████ \n" + + " ██ \n" + + " ██ \n") + } + + func showMessage(s string) { fmt.Printf("*** %s\n\n", s) } + + func main() { + var sdkKey = os.Getenv("LAUNCHDARKLY_SDK_KEY") + + if sdkKey == "" { + showMessage("LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.") + os.Exit(1) + } + + ldClient, _ := ld.MakeClient(sdkKey, 5*time.Second) + if ldClient.Initialized() { + showMessage("SDK successfully initialized!") + } else { + showMessage("SDK failed to initialize") + os.Exit(1) + } + + // Set up the evaluation context. This context should appear on your LaunchDarkly contexts dashboard + // soon after you run the demo. + context := ldcontext.NewBuilder("example-user-key"). + Name("Sandy"). + Build() + + // Set featureFlagKey to the feature flag key you want to evaluate. + var featureFlagKey = "{{ featureKey }}" + + if os.Getenv("LAUNCHDARKLY_FLAG_KEY") != "" { + featureFlagKey = os.Getenv("LAUNCHDARKLY_FLAG_KEY") + } + + flagValue, err := ldClient.BoolVariation(featureFlagKey, context, false) + if err != nil { + showMessage("error: " + err.Error()) + } + + showMessage(fmt.Sprintf("The '%s' feature flag evaluates to %t.", featureFlagKey, flagValue)) + + if flagValue { + showBanner() + } + + if os.Getenv("CI") != "" { + os.Exit(0) + } + + updateCh := ldClient.GetFlagTracker().AddFlagValueChangeListener(featureFlagKey, context, ldvalue.Null()) + + for event := range updateCh { + showMessage(fmt.Sprintf("The '%s' feature flag evaluates to %t.", featureFlagKey, event.NewValue.BoolValue())) + if event.NewValue.BoolValue() { + showBanner() + } + } + } +``` diff --git a/snippets/sdks/go-server-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/go-server-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..6ad5d59 --- /dev/null +++ b/snippets/sdks/go-server-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: go-server-sdk/getting-started/mkdir +sdk: go-server-sdk +kind: bootstrap +lang: shell +description: Create the project directory for the Go hello-world. +ld-application: + slot: mkdir +--- + +Create a new directory for your application: + +```shell +mkdir hello-go && cd hello-go +``` diff --git a/snippets/sdks/go-server-sdk/snippets/getting-started/mod-init.snippet.md b/snippets/sdks/go-server-sdk/snippets/getting-started/mod-init.snippet.md new file mode 100644 index 0000000..e631974 --- /dev/null +++ b/snippets/sdks/go-server-sdk/snippets/getting-started/mod-init.snippet.md @@ -0,0 +1,15 @@ +--- +id: go-server-sdk/getting-started/mod-init +sdk: go-server-sdk +kind: bootstrap +lang: shell +description: Initialize a Go module for the project. +ld-application: + slot: mod-init +--- + +Start your module using the [`go mod init`](https://go.dev/ref/mod#go-mod-init) command: + +```shell +go mod init example/hello-go +``` diff --git a/snippets/sdks/go-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/go-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..3de1435 --- /dev/null +++ b/snippets/sdks/go-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: go-server-sdk/getting-started/run +sdk: go-server-sdk +kind: run +lang: shell +description: Build and run the program with the SDK key in the environment. +inputs: + apiKey: + type: sdk-key + description: SDK key to embed in the rendered Run command. +ld-application: + slot: run +--- + +Build and run: + +```shell +go build && LAUNCHDARKLY_SDK_KEY='{{ apiKey }}' ./hello-go +``` diff --git a/snippets/sdks/haskell-server-sdk/sdk.yaml b/snippets/sdks/haskell-server-sdk/sdk.yaml new file mode 100644 index 0000000..69d7d0f --- /dev/null +++ b/snippets/sdks/haskell-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: haskell-server-sdk +sdk-meta-id: haskell +display-name: Haskell +type: server-side +languages: + - id: haskell + extensions: [".hs"] +package-managers: [stack, cabal] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/haskell.tsx +docs: + reference-page: /sdk/server-side/haskell +hello-world-repo: launchdarkly/hello-haskell-server diff --git a/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md b/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md new file mode 100644 index 0000000..6b624d2 --- /dev/null +++ b/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md @@ -0,0 +1,103 @@ +--- +id: haskell-server-sdk/getting-started/main-hs +sdk: haskell-server-sdk +kind: hello-world +lang: haskell +file: app/Main.hs +description: Hello-world program that initializes the Haskell server SDK and watches a feature flag. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Gonfalon's snippet uses this as the env-var name passed to lookupEnv (see comment below); validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: main-hs +# validation runtime not yet wired — Haskell stack-build harness pending. +# When added, the stack image needs launchdarkly-server-sdk + text +# pre-installed to keep per-validate cost reasonable. +--- + +Edit `app/Main.hs` by adding the following code: + +```haskell +{-# LANGUAGE OverloadedStrings, NumericUnderscores #-} +module Main where +import Control.Concurrent (threadDelay) +import Control.Monad (forever) +import Data.Text (Text, pack) +import Data.Function ((&)) +import qualified LaunchDarkly.Server as LD +import System.Timeout (timeout) +import Text.Printf (printf, hPrintf) +import System.Environment (lookupEnv) + +showEvaluationResult :: String -> Bool -> IO () +showEvaluationResult key value = do + printf "*** The %s feature flag evaluates to %s\n" key (show value) + +showBanner :: IO () +showBanner = putStr "\n\ +\ ██ \n\ +\ ██ \n\ +\ ████████ \n\ +\ ███████ \n\ +\██ LAUNCHDARKLY █\n\ +\ ███████ \n\ +\ ████████ \n\ +\ ██ \n\ +\ ██ \n\ +\\n\ +\" + +showMessage :: String -> Bool -> Maybe Bool -> Bool -> IO Bool +showMessage key True _ True = do + showBanner + showEvaluationResult key True + pure False +showMessage key value Nothing showBanner = do + showEvaluationResult key value + pure showBanner +showMessage key value (Just lastValue) showBanner + | value /= lastValue = do + showEvaluationResult key value + pure showBanner + | otherwise = pure showBanner + +waitForClient :: LD.Client -> IO Bool +waitForClient client = do + status <- LD.getStatus client + case status of + LD.Uninitialized -> threadDelay (1 * 1_000) >> waitForClient client + LD.Initialized -> return True + _anyOtherStatus -> return False + +evaluateLoop :: LD.Client -> String -> LD.Context -> Maybe Bool -> Bool -> IO () +evaluateLoop client featureFlagKey context lastValue showBanner = do + value <- LD.boolVariation client (pack featureFlagKey) context False + showBanner' <- showMessage featureFlagKey value lastValue showBanner + + threadDelay (1 * 1_000_000) >> evaluateLoop client featureFlagKey context (Just value) showBanner' + +evaluate :: Maybe String -> Maybe String -> IO () +evaluate (Just sdkKey) Nothing = do evaluate (Just sdkKey) (Just "sample-feature") +evaluate (Just sdkKey) (Just featureFlagKey) = do + -- Set up the evaluation context. This context should appear on your + -- LaunchDarkly contexts dashboard soon after you run the demo. + let context = LD.makeContext "example-user-key" "user" & LD.withName "Sandy" + client <- LD.makeClient $ LD.makeConfig (pack sdkKey) + initialized <- timeout (5_000 * 1_000) (waitForClient client) + + case initialized of + Just True -> do + print "*** SDK successfully initialized!" + evaluateLoop client featureFlagKey context Nothing True + _notInitialized -> putStrLn "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo." +evaluate _ _ = putStrLn "*** You must define LAUNCHDARKLY_SDK_KEY and LAUNCHDARKLY_FLAG_KEY before running this script" + +main :: IO () +main = do + -- Set sdkKey to your LaunchDarkly SDK key. + sdkKey <- lookupEnv "LAUNCHDARKLY_SDK_KEY" + -- Set featureFlagKey to the feature flag key you want to evaluate. + featureFlagKey <- lookupEnv "{{ featureKey }}" + evaluate sdkKey featureFlagKey +``` diff --git a/snippets/sdks/haskell-server-sdk/snippets/getting-started/package-yaml.snippet.md b/snippets/sdks/haskell-server-sdk/snippets/getting-started/package-yaml.snippet.md new file mode 100644 index 0000000..3c2195d --- /dev/null +++ b/snippets/sdks/haskell-server-sdk/snippets/getting-started/package-yaml.snippet.md @@ -0,0 +1,15 @@ +--- +id: haskell-server-sdk/getting-started/package-yaml +sdk: haskell-server-sdk +kind: manifest-fragment +lang: yaml +description: Dependencies entry to add to package.yaml. +ld-application: + slot: package-yaml +--- + +Next, add the SDK and `text` package to your list of dependencies in `package.yaml`: + +```yaml +launchdarkly-server-sdk, text +``` diff --git a/snippets/sdks/haskell-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/haskell-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..7b341e3 --- /dev/null +++ b/snippets/sdks/haskell-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: haskell-server-sdk/getting-started/run +sdk: haskell-server-sdk +kind: run +lang: shell +description: Build with stack and run. +inputs: + apiKey: + type: sdk-key + description: SDK key embedded in the rendered Run command. +ld-application: + slot: run +--- + +Build and run: + +```shell +stack build && LAUNCHDARKLY_SDK_KEY='{{ apiKey }}' stack exec hello-haskell-exe +``` diff --git a/snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-new.snippet.md b/snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-new.snippet.md new file mode 100644 index 0000000..ae0decf --- /dev/null +++ b/snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-new.snippet.md @@ -0,0 +1,15 @@ +--- +id: haskell-server-sdk/getting-started/stack-new +sdk: haskell-server-sdk +kind: bootstrap +lang: shell +description: Bootstrap a Haskell stack project. +ld-application: + slot: stack-new +--- + +Create a new project for your application: + +```shell +stack new hello-haskell && cd hello-haskell +``` diff --git a/snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-yaml.snippet.md b/snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-yaml.snippet.md new file mode 100644 index 0000000..99b0e63 --- /dev/null +++ b/snippets/sdks/haskell-server-sdk/snippets/getting-started/stack-yaml.snippet.md @@ -0,0 +1,20 @@ +--- +id: haskell-server-sdk/getting-started/stack-yaml +sdk: haskell-server-sdk +kind: manifest-fragment +lang: yaml +description: extra-deps entry to add to stack.yaml. +inputs: + version: + type: string + description: SDK version. Gonfalon fetches the latest from Hackage asynchronously. + runtime-default: "" +ld-application: + slot: stack-yaml +--- + +Add the SDK version as an `extra-deps` entry in `stack.yaml`: + +```yaml +- launchdarkly-server-sdk-{{ version }} +``` diff --git a/snippets/sdks/ios-client-sdk/sdk.yaml b/snippets/sdks/ios-client-sdk/sdk.yaml new file mode 100644 index 0000000..8d2e9bb --- /dev/null +++ b/snippets/sdks/ios-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: ios-client-sdk +sdk-meta-id: ios +display-name: iOS (Swift) +type: client-side +languages: + - id: swift + extensions: [".swift"] +package-managers: [cocoapods, spm, carthage] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/ios.tsx +docs: + reference-page: /sdk/client-side/ios +hello-world-repo: launchdarkly/hello-ios-swift diff --git a/snippets/sdks/ios-client-sdk/snippets/getting-started/app-delegate.snippet.md b/snippets/sdks/ios-client-sdk/snippets/getting-started/app-delegate.snippet.md new file mode 100644 index 0000000..266d0e9 --- /dev/null +++ b/snippets/sdks/ios-client-sdk/snippets/getting-started/app-delegate.snippet.md @@ -0,0 +1,52 @@ +--- +id: ios-client-sdk/getting-started/app-delegate +sdk: ios-client-sdk +kind: hello-world +lang: swift +file: AppDelegate.swift +description: AppDelegate that boots the LDClient on app launch. +inputs: + mobileKey: + type: mobile-key + description: Mobile key baked into the rendered source. +ld-application: + slot: app-delegate +# Validator pending — iOS validation requires a macOS runner with Xcode +# and `xcodebuild test` orchestration; deferred to a future slice. +--- + +Open `AppDelegate.swift` and add the following code: + +```swift +import UIKit +// Import the LaunchDarkly SDK. +import LaunchDarkly + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + // Set sdkKey to your LaunchDarkly mobile key. + private let sdkKey = "{{ mobileKey }}" + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + setUpLDClient() + + return true + } + + private func setUpLDClient() { + // Set up the evaluation context. This context should appear on your + // LaunchDarkly contexts dashboard soon after you run the demo. + var contextBuilder = LDContextBuilder(key: "example-user-key") + contextBuilder.kind("user") + contextBuilder.name("Sandy") + + guard case .success(let context) = contextBuilder.build() + else { return } + + let config = LDConfig(mobileKey: sdkKey, autoEnvAttributes: .enabled) + LDClient.start(config: config, context: context, startWaitSeconds: 30) + } +} +``` diff --git a/snippets/sdks/ios-client-sdk/snippets/getting-started/pod-install.snippet.md b/snippets/sdks/ios-client-sdk/snippets/getting-started/pod-install.snippet.md new file mode 100644 index 0000000..bc7e287 --- /dev/null +++ b/snippets/sdks/ios-client-sdk/snippets/getting-started/pod-install.snippet.md @@ -0,0 +1,15 @@ +--- +id: ios-client-sdk/getting-started/pod-install +sdk: ios-client-sdk +kind: install +lang: shell +description: Install pod dependencies. +ld-application: + slot: pod-install +--- + +Install the dependencies: + +```shell +pod install +``` diff --git a/snippets/sdks/ios-client-sdk/snippets/getting-started/podfile.snippet.md b/snippets/sdks/ios-client-sdk/snippets/getting-started/podfile.snippet.md new file mode 100644 index 0000000..7e90840 --- /dev/null +++ b/snippets/sdks/ios-client-sdk/snippets/getting-started/podfile.snippet.md @@ -0,0 +1,24 @@ +--- +id: ios-client-sdk/getting-started/podfile +sdk: ios-client-sdk +kind: manifest +lang: ruby +file: Podfile +description: CocoaPods Podfile pulling the LaunchDarkly iOS SDK. +inputs: + version: + type: string + description: SDK version. Defaults to '6.1.0' in gonfalon as a fallback when the async fetch hasn't completed. + runtime-default: "6.1.0" +ld-application: + slot: podfile +--- + +Install the LaunchDarkly SDK using [CocoaPods](https://cocoapods.org/) by creating a `Podfile`: + +```ruby +target 'hello-swift' do + pod 'LaunchDarkly', '{{ version }}' +end + +``` diff --git a/snippets/sdks/ios-client-sdk/snippets/getting-started/view-controller.snippet.md b/snippets/sdks/ios-client-sdk/snippets/getting-started/view-controller.snippet.md new file mode 100644 index 0000000..66a7ce7 --- /dev/null +++ b/snippets/sdks/ios-client-sdk/snippets/getting-started/view-controller.snippet.md @@ -0,0 +1,59 @@ +--- +id: ios-client-sdk/getting-started/view-controller +sdk: ios-client-sdk +kind: hello-world +lang: swift +file: ViewController.swift +description: ViewController that observes the flag and renders the value. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: view-controller +# Validator pending — same as app-delegate. +# +# Known bug in this snippet (carried verbatim from gonfalon for now): +# the updateUi label uses `(flagKey)` and `(result)` instead of Swift's +# string interpolation `\(flagKey)` and `\(result)`. As written it +# would print the literal text `(flagKey)`. Fix-on-red when the iOS +# validator lands. +--- + +Open `ViewController.swift` and add the following code: + +```swift +import UIKit +import LaunchDarkly + +class ViewController: UIViewController { + + @IBOutlet weak var featureFlagLabel: UILabel! + + // Set featureFlagKey to the feature flag key you want to evaluate. + fileprivate let featureFlagKey = "{{ featureKey }}" + + override func viewDidLoad() { + super.viewDidLoad() + + if let ld = LDClient.get() { + ld.observe(key: featureFlagKey, owner: self) { [weak self] changedFlag in + guard let me = self else { return } + guard case .bool(let booleanValue) = changedFlag.newValue else { return } + + me.updateUi(flagKey: changedFlag.key, result: booleanValue) + } + let result = ld.boolVariation(forKey: featureFlagKey, defaultValue: false) + updateUi(flagKey: featureFlagKey, result: result) + } + } + + func updateUi(flagKey: String, result: Bool) { + self.featureFlagLabel.text = "The (flagKey) feature flag evaluates to (result)" + + let toggleOn = UIColor(red: 0, green: 0.52, blue: 0.29, alpha: 1) + let toggleOff = UIColor(red: 0.22, green: 0.22, blue: 0.25, alpha: 1) + self.view.backgroundColor = result ? toggleOn : toggleOff + } +} +``` diff --git a/snippets/sdks/java-server-sdk/sdk.yaml b/snippets/sdks/java-server-sdk/sdk.yaml new file mode 100644 index 0000000..1382d62 --- /dev/null +++ b/snippets/sdks/java-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: java-server-sdk +sdk-meta-id: java +display-name: Java +type: server-side +languages: + - id: java + extensions: [".java"] +package-managers: [maven, gradle] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/java.tsx +docs: + reference-page: /sdk/server-side/java +hello-world-repo: launchdarkly/hello-java diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/app-java.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/app-java.snippet.md new file mode 100644 index 0000000..c5fcdc5 --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/app-java.snippet.md @@ -0,0 +1,135 @@ +--- +id: java-server-sdk/getting-started/app-java +sdk: java-server-sdk +kind: hello-world +lang: java +file: src/main/java/com/launchdarkly/tutorial/App.java +description: Hello-world program that initializes the Java server SDK and watches a feature flag. +inputs: + apiKey: + type: sdk-key + description: SDK key baked into the rendered source. Validation reads LAUNCHDARKLY_SDK_KEY at runtime; the env var takes precedence. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: app-java +validation: + runtime: jvm +--- + +Remove the prepopulated lines except the first line and add the following code to `App.java`: + +```java +import java.io.IOException; + + import com.launchdarkly.sdk.*; + import com.launchdarkly.sdk.server.*; + + public class App { + + // Set SDK_KEY to your LaunchDarkly SDK key. + static String SDK_KEY = "{{ apiKey }}"; + + // Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. + static String FEATURE_FLAG_KEY = "{{ featureKey }}"; + + private static void showMessage(String s) { + System.out.println("*** " + s); + System.out.println(); + } + + private static void showBanner() { + showMessage("\n ██ \n" + + " ██ \n" + + " ████████ \n" + + " ███████ \n" + + "██ LAUNCHDARKLY █\n" + + " ███████ \n" + + " ████████ \n" + + " ██ \n" + + " ██ \n"); + } + + public static void main(String... args) throws Exception { + boolean CIMode = System.getenv("CI") != null; + + String envSDKKey = System.getenv("LAUNCHDARKLY_SDK_KEY"); + if(envSDKKey != null) { + SDK_KEY = envSDKKey; + } + + String envFlagKey = System.getenv("LAUNCHDARKLY_FLAG_KEY"); + if(envFlagKey != null) { + FEATURE_FLAG_KEY = envFlagKey; + } + + LDConfig config = new LDConfig.Builder().build(); + + if (SDK_KEY == null || SDK_KEY.equals("")) { + showMessage("Please set the LAUNCHDARKLY_SDK_KEY environment variable or edit Hello.java to set SDK_KEY to your LaunchDarkly SDK key first."); + System.exit(1); + } + + final LDClient client = new LDClient(SDK_KEY, config); + if (client.isInitialized()) { + showMessage("SDK successfully initialized!"); + } else { + showMessage("SDK failed to initialize. Please check your internet connection and SDK credential for any typo."); + System.exit(1); + } + + // Set up the evaluation context. This context should appear on your + // LaunchDarkly contexts dashboard soon after you run the demo. + final LDContext context = LDContext.builder("example-user-key") + .name("Sandy") + .build(); + + // Evaluate the feature flag for this context. + boolean flagValue = client.boolVariation(FEATURE_FLAG_KEY, context, false); + showMessage("The '" + FEATURE_FLAG_KEY + "' feature flag evaluates to " + flagValue + "."); + + if (flagValue) { + showBanner(); + } + + //If this is building for CI, we don't need to keep running the Hello App continously. + if(CIMode) { + System.exit(0); + } + + // We set up a flag change listener so you can see flag changes as you change + // the flag rules. + client.getFlagTracker().addFlagValueChangeListener(FEATURE_FLAG_KEY, context, event -> { + showMessage("The '" + FEATURE_FLAG_KEY + "' feature flag evaluates to " + event.getNewValue() + "."); + + if (event.getNewValue().booleanValue()) { + showBanner(); + } + }); + showMessage("Listening for feature flag changes."); + + // Here we ensure that when the application terminates, the SDK shuts down + // cleanly and has a chance to deliver analytics events to LaunchDarkly. + // If analytics events are not delivered, the context attributes and flag usage + // statistics may not appear on your dashboard. In a normal long-running + // application, the SDK would continue running and events would be delivered + // automatically in the background. + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + public void run() { + try { + client.close(); + } catch (IOException e) { + // ignore + } + } + }, "ldclient-cleanup-thread")); + + // Keeps example application alive. + Object mon = new Object(); + synchronized (mon) { + mon.wait(); + } + } + } +``` diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/cd-into.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/cd-into.snippet.md new file mode 100644 index 0000000..1fdcf45 --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/cd-into.snippet.md @@ -0,0 +1,15 @@ +--- +id: java-server-sdk/getting-started/cd-into +sdk: java-server-sdk +kind: bootstrap +lang: shell +description: Change into the project directory. +ld-application: + slot: cd-into +--- + +Change into the project directory: + +```shell +cd hello-java +``` diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/mvn-generate.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/mvn-generate.snippet.md new file mode 100644 index 0000000..b418180 --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/mvn-generate.snippet.md @@ -0,0 +1,15 @@ +--- +id: java-server-sdk/getting-started/mvn-generate +sdk: java-server-sdk +kind: bootstrap +lang: shell +description: Bootstrap a Maven project. +ld-application: + slot: mvn-generate +--- + +Create a new project and accept the default options suggested by maven: + +```shell +mvn archetype:generate -DgroupId=com.launchdarkly.tutorial -DartifactId=hello-java +``` diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/pom-build.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/pom-build.snippet.md new file mode 100644 index 0000000..f46f095 --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/pom-build.snippet.md @@ -0,0 +1,31 @@ +--- +id: java-server-sdk/getting-started/pom-build +sdk: java-server-sdk +kind: manifest-fragment +lang: xml +description: pom.xml `` block configuring the maven-assembly-plugin. +ld-application: + slot: pom-build +--- + +Configure the Maven Assembly Plugin in your `pom.xml` to make it easier to run the application: + +```xml + + + + maven-assembly-plugin + + + + com.launchdarkly.tutorial.App + + + + jar-with-dependencies + + + + + +``` diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/pom-compiler.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/pom-compiler.snippet.md new file mode 100644 index 0000000..9d5229c --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/pom-compiler.snippet.md @@ -0,0 +1,16 @@ +--- +id: java-server-sdk/getting-started/pom-compiler +sdk: java-server-sdk +kind: manifest-fragment +lang: xml +description: pom.xml maven-compiler source/target levels. +ld-application: + slot: pom-compiler +--- + +Depending on your Java version, you may need to change the compilation source and target level in `pom.xml`: + +```xml +1.8 + 1.8 +``` diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/pom-dependency.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/pom-dependency.snippet.md new file mode 100644 index 0000000..bad92d3 --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/pom-dependency.snippet.md @@ -0,0 +1,24 @@ +--- +id: java-server-sdk/getting-started/pom-dependency +sdk: java-server-sdk +kind: manifest-fragment +lang: xml +description: pom.xml `` entry for the Java server SDK. +inputs: + version: + type: string + description: SDK version. Gonfalon fetches the latest from Maven Central asynchronously; renders as empty during the fetch (rather than the prior stale '5.0.0' fallback). + runtime-default: "" +ld-application: + slot: pom-dependency +--- + +Add the SDK to your project in your `pom.xml ` section: + +```xml + + com.launchdarkly + launchdarkly-java-server-sdk + {{ version }} + +``` diff --git a/snippets/sdks/java-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/java-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..cd32a8e --- /dev/null +++ b/snippets/sdks/java-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: java-server-sdk/getting-started/run +sdk: java-server-sdk +kind: run +lang: shell +description: Build the assembly jar and run with the SDK key in the environment. +inputs: + apiKey: + type: sdk-key + description: SDK key embedded in the rendered Run command. +ld-application: + slot: run +--- + +Build and run: + +```shell +mvn clean compile assembly:single && LAUNCHDARKLY_SDK_KEY={{ apiKey }} java -jar target/hello-java-1.0-SNAPSHOT-jar-with-dependencies.jar +``` diff --git a/snippets/sdks/js-client-sdk/sdk.yaml b/snippets/sdks/js-client-sdk/sdk.yaml new file mode 100644 index 0000000..d1aded9 --- /dev/null +++ b/snippets/sdks/js-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: js-client-sdk +sdk-meta-id: js +display-name: JavaScript (browser) +type: client-side +languages: + - id: html + extensions: [".html"] +package-managers: [cdn, npm] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/javascript.tsx +docs: + reference-page: /sdk/client-side/javascript +hello-world-repo: launchdarkly/hello-js diff --git a/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md b/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md new file mode 100644 index 0000000..185e44e --- /dev/null +++ b/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md @@ -0,0 +1,85 @@ +--- +id: js-client-sdk/getting-started/index-html +sdk: js-client-sdk +kind: hello-world +lang: html +file: index.html +description: Hello-world HTML page that initializes the JavaScript client SDK and renders the flag value. +inputs: + environmentId: + type: client-side-id + description: Client-side ID baked into the rendered source. Validation reads LAUNCHDARKLY_CLIENT_SIDE_ID at runtime. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: index-html +validation: + runtime: browser +--- + +Create a file called `index.html` and add the following code: + +```html + + + + + + LaunchDarkly tutorial + + + + + + +``` diff --git a/snippets/sdks/lua-server-sdk/sdk.yaml b/snippets/sdks/lua-server-sdk/sdk.yaml new file mode 100644 index 0000000..6f53ad3 --- /dev/null +++ b/snippets/sdks/lua-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: lua-server-sdk +sdk-meta-id: lua +display-name: Lua +type: server-side +languages: + - id: lua + extensions: [".lua"] +package-managers: [luarocks] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/luaServer.tsx +docs: + reference-page: /sdk/server-side/lua +hello-world-repo: launchdarkly/lua-server-sdk diff --git a/snippets/sdks/lua-server-sdk/snippets/getting-started/cpp-build.snippet.md b/snippets/sdks/lua-server-sdk/snippets/getting-started/cpp-build.snippet.md new file mode 100644 index 0000000..a7f0a89 --- /dev/null +++ b/snippets/sdks/lua-server-sdk/snippets/getting-started/cpp-build.snippet.md @@ -0,0 +1,27 @@ +--- +id: lua-server-sdk/getting-started/cpp-build +sdk: lua-server-sdk +kind: install +lang: bash +description: Compile and install the underlying C++ Server SDK from source. +ld-application: + slot: cpp-build +--- + +If the C++ Server SDK is already installed or you already obtained release artifacts from LaunchDarkly, skip this step. + +Otherwise, compile and install the C++ Server SDK: + +```bash + +git clone https://github.com/launchdarkly/cpp-sdks.git && cd cpp-sdks +mkdir build && cd build +cmake -G Ninja -D BUILD_TESTING=OFF \ + -D CMAKE_BUILD_TYPE=Release \ + -D LD_BUILD_SHARED_LIBS=On \ + -D CMAKE_INSTALL_PREFIX=./install .. +cmake --build . --target launchdarkly-cpp-server +cmake --install . +cd ../../ + +``` diff --git a/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md b/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md new file mode 100644 index 0000000..e152ab8 --- /dev/null +++ b/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md @@ -0,0 +1,39 @@ +--- +id: lua-server-sdk/getting-started/hello-lua +sdk: lua-server-sdk +kind: hello-world +lang: lua +file: hello.lua +description: Hello-world program that initializes the Lua server SDK and evaluates a feature flag. +inputs: + apiKey: + type: sdk-key + description: SDK key baked into the rendered source. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: hello-lua +# Validator pending. Lua wraps the C++ Server SDK and requires the +# C++ build (cmake + boost + openssl + ninja) plus luarocks. A +# specialized validator image is needed; deferred. +--- + +Create a file named `hello.lua` and add the following code: + +```lua +local ld = require("launchdarkly_server_sdk") +local config = {} + +local client = ld.clientInit("{{ apiKey }}", 1000, config) + +local user = ld.makeContext({ + user = { + key = "example-user-key", + name = "Sandy" + } +}) + +local value = client:boolVariation(user, "{{ featureKey }}", false) +print("Feature flag '{{ featureKey }}' is "..tostring(value).."") +``` diff --git a/snippets/sdks/lua-server-sdk/snippets/getting-started/luarocks-install.snippet.md b/snippets/sdks/lua-server-sdk/snippets/getting-started/luarocks-install.snippet.md new file mode 100644 index 0000000..6e68f9c --- /dev/null +++ b/snippets/sdks/lua-server-sdk/snippets/getting-started/luarocks-install.snippet.md @@ -0,0 +1,15 @@ +--- +id: lua-server-sdk/getting-started/luarocks-install +sdk: lua-server-sdk +kind: install +lang: bash +description: Install the Lua server SDK via luarocks. +ld-application: + slot: luarocks-install +--- + +Download the Lua Server SDK and build it with `luarocks` (replace `LD_DIR` with the path to the C++ SDK's shared libraries as necessary): + +```bash +luarocks install launchdarkly-server-sdk LD_DIR="$(pwd)/cpp-sdks/build/install" +``` diff --git a/snippets/sdks/lua-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/lua-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..b30e2eb --- /dev/null +++ b/snippets/sdks/lua-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,15 @@ +--- +id: lua-server-sdk/getting-started/run +sdk: lua-server-sdk +kind: run +lang: bash +description: Run the Lua program. +ld-application: + slot: run +--- + +Run: + +```bash +lua hello.lua +``` diff --git a/snippets/sdks/node-client-sdk/sdk.yaml b/snippets/sdks/node-client-sdk/sdk.yaml new file mode 100644 index 0000000..bf402bb --- /dev/null +++ b/snippets/sdks/node-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: node-client-sdk +sdk-meta-id: node-client +display-name: Node.js (client-side) +type: client-side +languages: + - id: javascript + extensions: [".js"] +package-managers: [npm] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/nodeClient.tsx +docs: + reference-page: /sdk/client-side/node-js +hello-world-repo: launchdarkly/hello-node-client diff --git a/snippets/sdks/node-client-sdk/snippets/getting-started/index-js.snippet.md b/snippets/sdks/node-client-sdk/snippets/getting-started/index-js.snippet.md new file mode 100644 index 0000000..3f7e834 --- /dev/null +++ b/snippets/sdks/node-client-sdk/snippets/getting-started/index-js.snippet.md @@ -0,0 +1,57 @@ +--- +id: node-client-sdk/getting-started/index-js +sdk: node-client-sdk +kind: hello-world +lang: javascript +file: index.js +description: Hello-world program that initializes the Node.js client SDK and evaluates a feature flag. +inputs: + environmentId: + type: client-side-id + description: Client-side ID baked into the rendered source. Validation reads LAUNCHDARKLY_CLIENT_SIDE_ID at runtime. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: index-js +validation: + runtime: node + requirements: launchdarkly-node-client-sdk +--- + +Create a file called `index.js` and add the following code: + +```javascript +// Import the LaunchDarkly client +var LaunchDarkly = require('launchdarkly-node-client-sdk'); + +// Set up the user properties. This user should appear on your LaunchDarkly users dashboard +// soon after you run the demo. +var user = { + key: "example-user-key" +}; + +// Create a single instance of the LaunchDarkly client +const ldClient = LaunchDarkly.initialize('{{ environmentId }}', user); + +function showMessage(s) { + console.log("*** " + s); + console.log(""); +} +ldClient.waitForInitialization().then(function() { + showMessage("SDK successfully initialized!"); + const flagValue = ldClient.variation("{{ featureKey }}", false); + + showMessage("The '" + "{{ featureKey }}" + "' feature flag evaluates to " + flagValue + "."); + + // Here we ensure that the SDK shuts down cleanly and has a chance to deliver analytics + // events to LaunchDarkly before the program exits. If analytics events are not delivered, + // the user properties and flag usage statistics will not appear on your dashboard. In a + // normal long-running application, the SDK would continue running and events would be + // delivered automatically in the background. + ldClient.close(); +}).catch(function(error) { + showMessage("SDK failed to initialize: " + error); + process.exit(1); +}); +``` diff --git a/snippets/sdks/node-client-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/node-client-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..2fa4805 --- /dev/null +++ b/snippets/sdks/node-client-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,20 @@ +--- +id: node-client-sdk/getting-started/install +sdk: node-client-sdk +kind: install +lang: shell +description: Install the Node.js client SDK. +inputs: + version: + type: string + description: Optional pinned SDK version; when empty the pin is omitted. + runtime-default: "" +ld-application: + slot: install +--- + +Next, install the LaunchDarkly SDK: + +```shell +npm install launchdarkly-node-client-sdk{{ if version }}@{{ version }}{{ end }} --save +``` diff --git a/snippets/sdks/node-client-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/node-client-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..7d85205 --- /dev/null +++ b/snippets/sdks/node-client-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: node-client-sdk/getting-started/mkdir +sdk: node-client-sdk +kind: bootstrap +lang: shell +description: Create the project directory and a package.json. +ld-application: + slot: mkdir +--- + +Create a new directory and create a `package.json` file: + +```shell +mkdir hello-node-client && cd hello-node-client && npm init +``` diff --git a/snippets/sdks/node-client-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/node-client-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..f668185 --- /dev/null +++ b/snippets/sdks/node-client-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,15 @@ +--- +id: node-client-sdk/getting-started/run +sdk: node-client-sdk +kind: run +lang: shell +description: Run the program. +ld-application: + slot: run +--- + +Run: + +```shell +node index.js +``` diff --git a/snippets/sdks/node-server-sdk/sdk.yaml b/snippets/sdks/node-server-sdk/sdk.yaml new file mode 100644 index 0000000..56251c3 --- /dev/null +++ b/snippets/sdks/node-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: node-server-sdk +sdk-meta-id: node-server +display-name: Node.js (server-side) +type: server-side +languages: + - id: javascript + extensions: [".js"] +package-managers: [npm] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/nodeServer.tsx +docs: + reference-page: /sdk/server-side/node-js +hello-world-repo: launchdarkly/hello-node-server diff --git a/snippets/sdks/node-server-sdk/snippets/getting-started/index-js.snippet.md b/snippets/sdks/node-server-sdk/snippets/getting-started/index-js.snippet.md new file mode 100644 index 0000000..dd78648 --- /dev/null +++ b/snippets/sdks/node-server-sdk/snippets/getting-started/index-js.snippet.md @@ -0,0 +1,88 @@ +--- +id: node-server-sdk/getting-started/index-js +sdk: node-server-sdk +kind: hello-world +lang: javascript +file: index.js +description: Hello-world program that initializes the Node.js server SDK and watches a feature flag. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: index-js +validation: + runtime: node + requirements: '@launchdarkly/node-server-sdk' +--- + +Create a file called `index.js` and add the following code: + +```javascript +const LaunchDarkly = require('@launchdarkly/node-server-sdk'); + +// Set sdkKey to your LaunchDarkly SDK key. +const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY ?? 'your-sdk-key'; + +// Set featureFlagKey to the feature flag key you want to evaluate. +const featureFlagKey = '{{ featureKey }}'; + +function showBanner() { + console.log( + ` ██ + ██ + ████████ + ███████ +██ LAUNCHDARKLY █ + ███████ + ████████ + ██ + ██ +`, + ); +} + +function printValueAndBanner(flagValue) { + console.log(`*** The '${featureFlagKey}' feature flag evaluates to ${flagValue}.`); + + if (flagValue) showBanner(); +} + +if (!sdkKey) { + console.log('*** Please edit index.js to set sdkKey to your LaunchDarkly SDK key first.'); + process.exit(1); +} + +const ldClient = LaunchDarkly.init(sdkKey); + +// Set up the context properties. This context should appear on your LaunchDarkly contexts dashboard +// soon after you run the demo. +const context = { + kind: 'user', + key: 'example-user-key', + name: 'Sandy', +}; + +ldClient + .waitForInitialization() + .then(() => { + console.log('*** SDK successfully initialized!'); + + const eventKey = `update:${featureFlagKey}`; + ldClient.on(eventKey, () => { + ldClient.variation(featureFlagKey, context, false).then(printValueAndBanner); + }); + + ldClient.variation(featureFlagKey, context, false).then((flagValue) => { + printValueAndBanner(flagValue); + + if(typeof process.env.CI !== "undefined") { + process.exit(0); + } + }); + }) + .catch((error) => { + console.log(`*** SDK failed to initialize: ${error}`); + process.exit(1); + }); +``` diff --git a/snippets/sdks/node-server-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/node-server-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..0c39e6b --- /dev/null +++ b/snippets/sdks/node-server-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,20 @@ +--- +id: node-server-sdk/getting-started/install +sdk: node-server-sdk +kind: install +lang: shell +description: Install the Node.js server SDK. +inputs: + version: + type: string + description: Optional pinned SDK version; when empty the pin is omitted. + runtime-default: "" +ld-application: + slot: install +--- + +Next, install the LaunchDarkly SDK: + +```shell +npm install @launchdarkly/node-server-sdk{{ if version }}@{{ version }}{{ end }} --save +``` diff --git a/snippets/sdks/node-server-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/node-server-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..cf618c8 --- /dev/null +++ b/snippets/sdks/node-server-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: node-server-sdk/getting-started/mkdir +sdk: node-server-sdk +kind: bootstrap +lang: shell +description: Create the project directory and a package.json. +ld-application: + slot: mkdir +--- + +Create a new directory and create a `package.json` file: + +```shell +mkdir hello-node-server && cd hello-node-server && npm init +``` diff --git a/snippets/sdks/node-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/node-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..15decb8 --- /dev/null +++ b/snippets/sdks/node-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: node-server-sdk/getting-started/run +sdk: node-server-sdk +kind: run +lang: shell +description: Run the program with the SDK key in the environment. +inputs: + apiKey: + type: sdk-key + description: SDK key embedded in the rendered Run command. +ld-application: + slot: run +--- + +Run: + +```shell +LAUNCHDARKLY_SDK_KEY={{ apiKey }} node index.js +``` diff --git a/snippets/sdks/php-server-sdk/sdk.yaml b/snippets/sdks/php-server-sdk/sdk.yaml new file mode 100644 index 0000000..4d18f66 --- /dev/null +++ b/snippets/sdks/php-server-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: php-server-sdk +sdk-meta-id: php +display-name: PHP +type: server-side +languages: + - id: php + extensions: [".php"] +package-managers: [composer] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/php.tsx +docs: + reference-page: /sdk/server-side/php +hello-world-repo: launchdarkly/hello-php diff --git a/snippets/sdks/php-server-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/php-server-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..d6d0893 --- /dev/null +++ b/snippets/sdks/php-server-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,20 @@ +--- +id: php-server-sdk/getting-started/install +sdk: php-server-sdk +kind: install +lang: shell +description: Install the LaunchDarkly SDK and Guzzle via composer. +inputs: + version: + type: string + description: Optional pinned SDK version; when empty the pin is omitted. + runtime-default: "" +ld-application: + slot: install +--- + +Next, install the LaunchDarkly SDK and Guzzle dependency: + +```shell +php composer.phar require launchdarkly/server-sdk{{ if version }}:{{ version }}{{ end }} guzzlehttp/guzzle +``` diff --git a/snippets/sdks/php-server-sdk/snippets/getting-started/main-php.snippet.md b/snippets/sdks/php-server-sdk/snippets/getting-started/main-php.snippet.md new file mode 100644 index 0000000..ffad011 --- /dev/null +++ b/snippets/sdks/php-server-sdk/snippets/getting-started/main-php.snippet.md @@ -0,0 +1,89 @@ +--- +id: php-server-sdk/getting-started/main-php +sdk: php-server-sdk +kind: hello-world +lang: php +file: main.php +description: Hello-world program that initializes the PHP server SDK and watches a feature flag. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. Validation reads LAUNCHDARKLY_FLAG_KEY at runtime. +ld-application: + slot: main-php +validation: + runtime: php + requirements: | + launchdarkly/server-sdk + guzzlehttp/guzzle +--- + +Create a file called `main.php` and add the following code: + +```php +kind("user") +->name("Sandy") +->build(); + + +$showBanner = true; +$lastValue = null; +do { + $flagValue = $client->variation($featureFlagKey, $context, false); + + if ($flagValue !== $lastValue) { + showEvaluationResult($featureFlagKey, $flagValue); + } + + if ($showBanner && $flagValue) { + showBanner(); + $showBanner = false; + } + + $lastValue = $flagValue; + sleep(1); +} while(true); +``` diff --git a/snippets/sdks/php-server-sdk/snippets/getting-started/mkdir.snippet.md b/snippets/sdks/php-server-sdk/snippets/getting-started/mkdir.snippet.md new file mode 100644 index 0000000..68ea1e9 --- /dev/null +++ b/snippets/sdks/php-server-sdk/snippets/getting-started/mkdir.snippet.md @@ -0,0 +1,15 @@ +--- +id: php-server-sdk/getting-started/mkdir +sdk: php-server-sdk +kind: bootstrap +lang: shell +description: Create the project directory and install Composer. +ld-application: + slot: mkdir +--- + +Create a new directory and install [Composer](https://getcomposer.org/): + +```shell +mkdir hello-php && cd hello-php && curl -sS https://getcomposer.org/installer | php +``` diff --git a/snippets/sdks/php-server-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/php-server-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..6687980 --- /dev/null +++ b/snippets/sdks/php-server-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,19 @@ +--- +id: php-server-sdk/getting-started/run +sdk: php-server-sdk +kind: run +lang: shell +description: Run the program with the SDK key in the environment. +inputs: + apiKey: + type: sdk-key + description: SDK key embedded in the rendered Run command. +ld-application: + slot: run +--- + +Run: + +```shell +LAUNCHDARKLY_SDK_KEY='{{ apiKey }}' php main.php +``` diff --git a/snippets/sdks/react-client-sdk/sdk.yaml b/snippets/sdks/react-client-sdk/sdk.yaml new file mode 100644 index 0000000..c35c006 --- /dev/null +++ b/snippets/sdks/react-client-sdk/sdk.yaml @@ -0,0 +1,22 @@ +id: react-client-sdk +sdk-meta-id: react-web +display-name: React (web) +type: client-side +languages: + - id: tsx + extensions: [".tsx"] + - id: jsx + extensions: [".jsx"] +package-managers: [npm, yarn] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + # Legacy variant (create-react-app) uses standard JSX template-literal + # interpolation and is rendered into gonfalon via render markers. The + # createApp (Vite) variant uses `?raw` asset-file imports + replaceAll; + # the snippet files are present in this dir but rendering into + # createApp.tsx is deferred until that file is migrated off the + # assetSource pattern. + get-started-file: static/ld/components/getStarted/sdk/react/legacy.tsx +docs: + reference-page: /sdk/client-side/react/react-web +hello-world-repo: launchdarkly-labs/react-ts diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md new file mode 100644 index 0000000..7783503 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -0,0 +1,32 @@ +--- +id: react-client-sdk/getting-started/app-tsx +sdk: react-client-sdk +kind: hello-world +lang: tsx +file: src/App.tsx +description: App component that uses useFlags to render the flag value. +inputs: + featureKey: + type: flag-key + description: Default flag key (camelCased) baked into the rendered source. +ld-application: + slot: app-tsx +--- + +Use the `useFlags` hook to evaluate flags. For example, in `App.tsx`: + +```tsx +import { useFlags } from 'launchdarkly-react-client-sdk'; + +function App() { + const { {{ featureKey }} } = useFlags(); + + return ( +
+ The {{ featureKey }} feature flag evaluates to { {{ featureKey }} ? 'true' : 'false'} +
+ ); +} + +export default App; +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/create-vite.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/create-vite.snippet.md new file mode 100644 index 0000000..f83e807 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/create-vite.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-client-sdk/getting-started/create-vite +sdk: react-client-sdk +kind: bootstrap +lang: shell +description: Bootstrap a React+TypeScript app with create-vite (createApp variant). +ld-application: + slot: create-vite +--- + +Use `create-vite` to create a new React Typescript application: + +```shell +npm create vite@latest hello-react-ts -- --template react-ts && cd hello-react-ts +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..d1199c9 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-client-sdk/getting-started/install +sdk: react-client-sdk +kind: install +lang: shell +description: Install the LaunchDarkly React SDK. +ld-application: + slot: install +--- + +Install the LaunchDarkly SDK: + +```shell +npm i --save launchdarkly-react-client-sdk +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md new file mode 100644 index 0000000..0e80977 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md @@ -0,0 +1,35 @@ +--- +id: react-client-sdk/getting-started/legacy-app-tsx +sdk: react-client-sdk +kind: hello-world +lang: tsx +file: src/App.tsx +description: CRA App.tsx (legacy variant) using useFlags to render the flag value. +inputs: + featureKey: + type: flag-key + description: Default flag key (camelCased) baked into the rendered source. Note that gonfalon camel-cases the supplied flag key before substituting; for validation we use the env-var value as-is. +ld-application: + slot: legacy-app-tsx +--- + +In `App.tsx`: + +```tsx +import './App.css'; +import { useFlags } from 'launchdarkly-react-client-sdk'; + +function App() { + const { {{ featureKey }} } = useFlags(); + + return ( +
+
+

The {{ featureKey }} feature flag evaluates to { {{ featureKey }} ? 'True' : 'False'}

+
+
+ ); +} + +export default App; +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-create.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-create.snippet.md new file mode 100644 index 0000000..5ea5c83 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-create.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-client-sdk/getting-started/legacy-create +sdk: react-client-sdk +kind: bootstrap +lang: shell +description: Bootstrap a React TypeScript app with create-react-app (legacy variant). +ld-application: + slot: legacy-create +--- + +Use `create-react-app` to create a new React application: + +```shell +npx create-react-app hello-react --template typescript && cd hello-react +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-index-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-index-tsx.snippet.md new file mode 100644 index 0000000..1727c20 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-index-tsx.snippet.md @@ -0,0 +1,44 @@ +--- +id: react-client-sdk/getting-started/legacy-index-tsx +sdk: react-client-sdk +kind: hello-world +lang: tsx +file: src/index.tsx +description: CRA index.tsx (legacy variant) wrapping the app with asyncWithLDProvider. +inputs: + environmentId: + type: client-side-id + description: Client-side ID baked into the rendered source. +ld-application: + slot: legacy-index-tsx +--- + +In `index.tsx`: + +```tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; +import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; + +(async () => { + const LDProvider = await asyncWithLDProvider({ + clientSideID: '{{ environmentId }}', + context: { + kind: 'user', + key: 'example-user-key', + name: 'Sandy', + }, + }); + + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + root.render( + + + + + , + ); +})(); +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-install.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-install.snippet.md new file mode 100644 index 0000000..d2b074c --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-install.snippet.md @@ -0,0 +1,20 @@ +--- +id: react-client-sdk/getting-started/legacy-install +sdk: react-client-sdk +kind: install +lang: shell +description: Install the LaunchDarkly React SDK (legacy variant — versioned npm install). +inputs: + version: + type: string + description: Optional pinned SDK version; when empty the pin is omitted. + runtime-default: "" +ld-application: + slot: legacy-install +--- + +Install the LaunchDarkly SDK: + +```shell +npm install --save launchdarkly-react-client-sdk{{ if version }}@{{ version }}{{ end }} +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-run.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-run.snippet.md new file mode 100644 index 0000000..f2d78b6 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-run.snippet.md @@ -0,0 +1,13 @@ +--- +id: react-client-sdk/getting-started/legacy-run +sdk: react-client-sdk +kind: run +lang: bash +description: Start the create-react-app dev server. +ld-application: + slot: legacy-run +--- + +```bash +npm start +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md new file mode 100644 index 0000000..327c4a0 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md @@ -0,0 +1,32 @@ +--- +id: react-client-sdk/getting-started/main-tsx +sdk: react-client-sdk +kind: hello-world +lang: tsx +file: src/main.tsx +description: Vite-app entrypoint that wraps the React app with LDProvider. +inputs: + environmentId: + type: client-side-id + description: Client-side ID baked into the rendered source. +ld-application: + slot: main-tsx +# Validator pending — Vite build + Playwright headless harness deferred. +--- + +In `main.tsx`, wrap your application with `LDProvider`: + +```tsx +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { LDProvider } from 'launchdarkly-react-client-sdk'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + + + , +); +``` diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/run-dev.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/run-dev.snippet.md new file mode 100644 index 0000000..50cdd76 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/run-dev.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-client-sdk/getting-started/run-dev +sdk: react-client-sdk +kind: run +lang: shell +description: Start the Vite dev server. +ld-application: + slot: run-dev +--- + +Run the app: + +```shell +npm run dev +``` diff --git a/snippets/sdks/react-native-client-sdk/sdk.yaml b/snippets/sdks/react-native-client-sdk/sdk.yaml new file mode 100644 index 0000000..21c4938 --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: react-native-client-sdk +sdk-meta-id: react-native +display-name: React Native +type: client-side +languages: + - id: tsx + extensions: [".tsx"] +package-managers: [npm, yarn] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/reactNative.tsx +docs: + reference-page: /sdk/client-side/react-native +hello-world-repo: launchdarkly/js-core diff --git a/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md b/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md new file mode 100644 index 0000000..cec80a5 --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -0,0 +1,49 @@ +--- +id: react-native-client-sdk/getting-started/app-tsx +sdk: react-native-client-sdk +kind: hello-world +lang: tsx +file: App.tsx +description: Root component that wires the LDProvider with the React Native client. +inputs: + mobileKey: + type: mobile-key + description: Mobile key baked into the rendered source. +ld-application: + slot: app-tsx +# Validator pending — RN bundler + Expo boot is heavy; deferred. +--- + +In `App.tsx`: + +```tsx +import { + AutoEnvAttributes, + LDProvider, + ReactNativeLDClient, +} from '@launchdarkly/react-native-client-sdk'; + +import Welcome from './src/welcome'; + +const featureClient = new ReactNativeLDClient( + '{{ mobileKey }}', + AutoEnvAttributes.Enabled, + { + debug: true, + applicationInfo: { + id: 'ld-rn-test-app', + version: '0.0.1', + }, + }, +); + +const App = () => { + return ( + + + + ); +}; + +export default App; +``` diff --git a/snippets/sdks/react-native-client-sdk/snippets/getting-started/create-expo.snippet.md b/snippets/sdks/react-native-client-sdk/snippets/getting-started/create-expo.snippet.md new file mode 100644 index 0000000..950d398 --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/create-expo.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-native-client-sdk/getting-started/create-expo +sdk: react-native-client-sdk +kind: bootstrap +lang: shell +description: Bootstrap an Expo React Native TypeScript app. +ld-application: + slot: create-expo +--- + +Use `create-expo-app` to create a new Expo application: + +```shell +npx create-expo-app hello-react-native -t expo-template-blank-typescript && cd hello-react-native +``` diff --git a/snippets/sdks/react-native-client-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/react-native-client-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..b677686 --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-native-client-sdk/getting-started/install +sdk: react-native-client-sdk +kind: install +lang: shell +description: Install the LaunchDarkly React Native SDK. +ld-application: + slot: install +--- + +Install the LaunchDarkly SDK: + +```shell +yarn add @launchdarkly/react-native-client-sdk +``` diff --git a/snippets/sdks/react-native-client-sdk/snippets/getting-started/run.snippet.md b/snippets/sdks/react-native-client-sdk/snippets/getting-started/run.snippet.md new file mode 100644 index 0000000..04fc090 --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/run.snippet.md @@ -0,0 +1,15 @@ +--- +id: react-native-client-sdk/getting-started/run +sdk: react-native-client-sdk +kind: run +lang: shell +description: Run the React Native app on iOS via yarn. +ld-application: + slot: run +--- + +Run: + +```shell +yarn ios +``` diff --git a/snippets/sdks/react-native-client-sdk/snippets/getting-started/welcome-tsx.snippet.md b/snippets/sdks/react-native-client-sdk/snippets/getting-started/welcome-tsx.snippet.md new file mode 100644 index 0000000..d66d45c --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/welcome-tsx.snippet.md @@ -0,0 +1,42 @@ +--- +id: react-native-client-sdk/getting-started/welcome-tsx +sdk: react-native-client-sdk +kind: hello-world +lang: tsx +file: src/welcome.tsx +description: Welcome component that evaluates the flag and renders the value. +inputs: + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: welcome-tsx +--- + +Create a new file `src/welcome.tsx`: + +```tsx +import {useEffect} from 'react'; +import {Text, View} from 'react-native'; + +import {useBoolVariation, useLDClient} from '@launchdarkly/react-native-client-sdk'; + +export default function Welcome() { + const flagValue = useBoolVariation('{{ featureKey }}', false); + const ldc = useLDClient(); + + useEffect(() => { + ldc + .identify({kind: 'user', key: 'example-user-key', name: 'Sandy'}) + .catch((e: any) => console.error('error: ' + e)); + }, []); + + return ( + + The {{ featureKey }} feature flag + evaluates to {flagValue ? 'true' : 'false'} + + ); +} +``` diff --git a/snippets/sdks/roku-client-sdk/sdk.yaml b/snippets/sdks/roku-client-sdk/sdk.yaml new file mode 100644 index 0000000..694416b --- /dev/null +++ b/snippets/sdks/roku-client-sdk/sdk.yaml @@ -0,0 +1,14 @@ +id: roku-client-sdk +sdk-meta-id: roku +display-name: Roku +type: client-side +languages: + - id: brightscript + extensions: [".brs"] +package-managers: [] +regions: [commercial, federal, eu, relay-proxy] +ld-application: + get-started-file: static/ld/components/getStarted/sdk/roku.tsx +docs: + reference-page: /sdk/client-side/roku +hello-world-repo: launchdarkly/hello-roku diff --git a/snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-brs.snippet.md b/snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-brs.snippet.md new file mode 100644 index 0000000..e43ca87 --- /dev/null +++ b/snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-brs.snippet.md @@ -0,0 +1,81 @@ +--- +id: roku-client-sdk/getting-started/app-scene-brs +sdk: roku-client-sdk +kind: hello-world +lang: brightscript +file: components/AppScene.brs +description: Scene-side logic that initializes the SDK and renders the flag value. +inputs: + mobileKey: + type: mobile-key + description: Mobile key baked into the rendered source. Roku snippets carry no automated validation — see the comment below the frontmatter. + featureKey: + type: flag-key + description: Default flag key baked into the rendered source. +ld-application: + slot: app-scene-brs +# validation: none +# +# Roku validation requires a Roku device or the proprietary BrightScript +# simulator (no public CI runtime exists). The snippet is rendered into +# gonfalon and is reviewed manually against a real Roku device when +# changed; the `version-staleness.yml` sweep still tracks the upstream +# package.zip release. See sdk-snippet-design.md §"Validator architecture" +# for the policy. +--- + +In `components/AppScene.brs` add the following code: + +```brightscript +function onFeatureChange() as Void + featureFlagKey = "{{ featureKey }}" + + value = m.ld.variation(featureFlagKey, false) + + if value then + m.top.backgroundColor = &h00844BFF + m.evaluation.text = "The " + featureFlagKey + " feature flag evaluates to true" + else + m.top.backgroundColor = &h373841FF + m.evaluation.text = "The " + featureFlagKey + " feature flag evaluates to false" + end if +end function + +function onStatusChange() as Void + if m.ld.status.getStatus() = m.ld.status.map.initialized + m.status.text = "SDK successfully initialized" + else + m.status.text = "SDK failed to initialize. Please check your internet connection and SDK credential for any typo." + end if +end function + +function init() as Void + mobileKey = "{{ mobileKey }}" + + launchDarklyNode = m.top.findNode("launchDarkly") + launchDarklyNode.observeField("flags", "onFeatureChange") + launchDarklyNode.observeField("status", "onStatusChange") + + config = LaunchDarklyConfig(mobileKey, launchDarklyNode) + + ' Set up the user-kind context properties. This context should appear on + ' your LaunchDarkly contexts dashboard soon after you run the demo. + context = LaunchDarklyCreateContext({kind: "user", key: "example-user-key", name: "Sandy"}) + LaunchDarklySGInit(config, context) + + m.ld = LaunchDarklySG(launchDarklyNode) + + m.evaluation = m.top.findNode("evaluation") + m.evaluation.font.size=40 + m.evaluation.color="0xFFFFFFFF" + + m.status = m.top.findNode("status") + m.status.font.size=20 + m.status.color="0xFFFFFFFF" + + m.top.backgroundColor = &h373841FF + m.top.backgroundUri = "" + + onFeatureChange() +end function +``` diff --git a/snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-xml.snippet.md b/snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-xml.snippet.md new file mode 100644 index 0000000..7415b4d --- /dev/null +++ b/snippets/sdks/roku-client-sdk/snippets/getting-started/app-scene-xml.snippet.md @@ -0,0 +1,45 @@ +--- +id: roku-client-sdk/getting-started/app-scene-xml +sdk: roku-client-sdk +kind: manifest +lang: xml +file: components/AppScene.xml +description: SceneGraph component definition with the LaunchDarklyTask node. +ld-application: + slot: app-scene-xml +--- + +In `components/AppScene.xml` create a basic scene by adding the following code: + +```xml + + + + + + + + + + +``` diff --git a/snippets/sdks/vue-client-sdk/snippets/getting-started/create-vue.snippet.md b/snippets/sdks/vue-client-sdk/snippets/getting-started/create-vue.snippet.md new file mode 100644 index 0000000..b9e524a --- /dev/null +++ b/snippets/sdks/vue-client-sdk/snippets/getting-started/create-vue.snippet.md @@ -0,0 +1,15 @@ +--- +id: vue-client-sdk/getting-started/create-vue +sdk: vue-client-sdk +kind: bootstrap +lang: shell +description: Bootstrap a new Vue project with create-vue. +ld-application: + slot: create-vue +--- + +Use create-vue to create a new Vue application: + +```shell +npm create vue@latest +``` diff --git a/snippets/sdks/vue-client-sdk/snippets/getting-started/install.snippet.md b/snippets/sdks/vue-client-sdk/snippets/getting-started/install.snippet.md new file mode 100644 index 0000000..bfeed36 --- /dev/null +++ b/snippets/sdks/vue-client-sdk/snippets/getting-started/install.snippet.md @@ -0,0 +1,15 @@ +--- +id: vue-client-sdk/getting-started/install +sdk: vue-client-sdk +kind: install +lang: shell +description: Change into the project and install the LaunchDarkly Vue SDK. +ld-application: + slot: install +--- + +Change dir and install the LaunchDarkly SDK: + +```shell +cd hello-vue && yarn add launchdarkly-vue-client-sdk +``` diff --git a/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md b/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md new file mode 100644 index 0000000..3da329b --- /dev/null +++ b/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md @@ -0,0 +1,30 @@ +--- +id: vue-client-sdk/getting-started/main-js +sdk: vue-client-sdk +kind: hello-world +lang: javascript +file: src/main.js +description: src/main.js wires the LDPlugin into the Vue app. +inputs: + environmentId: + type: client-side-id + description: Client-side ID baked into the rendered source. +ld-application: + slot: main-js +# Validator pending — Vue + Vite + Playwright harness deferred. +--- + +In `src/main.js`: + +```javascript +import { createApp } from 'vue' +import App from './App.vue' +import { LDPlugin } from 'launchdarkly-vue-client-sdk' + +const app = createApp(App) +app.use(LDPlugin, { + clientSideID: '{{ environmentId }}', + context: { kind: 'user', key: 'example-user' }, +}) +app.mount('#app') +``` From 27c36b49398278fb9c5145278bb8ab76041b66ce Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:57:11 -0700 Subject: [PATCH 02/19] feat(validators): per-runtime Docker harnesses for the new SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each `validators/languages//` carries a `runner.yaml` declaring `mode: docker`, an `image-prefix:` for the per-validator content-hash tag, and a `runs-on:` hint. The Dockerfile builds the language image (with the shared lib at `/harness-shared` per the build-context refactor in the parent branch) and the harness sources `/harness-shared/lib.sh` for the polling/timeout loop and key- redacting log dump. Runtimes added: - go → go-server-sdk - node → node-server-sdk + node-client-sdk - ruby → ruby-server-sdk (with $stdout.sync = true shim so block-buffered output reaches the matcher) - php → php-server-sdk (composer pre-installed) - dotnet-server → dotnet-server-sdk (synthesizes a minimal .csproj) - dotnet-client → dotnet-client-sdk (top-level-statements .csproj) - jvm → java-server-sdk (synthesizes pom.xml pinning LD java 7.13.4 + maven-assembly-plugin; harness prepends `package com.launchdarkly.tutorial;` to App.java since `mvn archetype:generate` writes that line for users) - rust → rust-server-sdk (cargo new + cargo add) - browser → js-client-sdk (Playwright v1.59.1 + Chromium; check.js loads file:///snippet/index.html in headless Chrome and polls page text for the success line) All eleven validators (python from the prior slice + ten new ones) were verified end-to-end against the real LD test environment except rust (cargo cold-build is multi-minute; CI exercises it). Each validator that surfaced a snippet bug got fixed via "fix on red" in the corresponding snippet — see the parent commit's notes. --- .../validators/languages/browser/Dockerfile | 13 +++ .../languages/browser/harness/check.js | 44 +++++++++ .../languages/browser/harness/run.sh | 12 +++ .../validators/languages/browser/runner.yaml | 3 + .../languages/dotnet-client/Dockerfile | 8 ++ .../languages/dotnet-client/harness/run.sh | 49 ++++++++++ .../languages/dotnet-client/runner.yaml | 3 + .../languages/dotnet-server/Dockerfile | 8 ++ .../languages/dotnet-server/harness/run.sh | 51 ++++++++++ .../languages/dotnet-server/runner.yaml | 3 + snippets/validators/languages/go/Dockerfile | 10 ++ .../validators/languages/go/harness/run.sh | 39 ++++++++ snippets/validators/languages/go/runner.yaml | 3 + snippets/validators/languages/jvm/Dockerfile | 8 ++ .../validators/languages/jvm/harness/run.sh | 96 +++++++++++++++++++ snippets/validators/languages/jvm/runner.yaml | 3 + snippets/validators/languages/node/Dockerfile | 8 ++ .../validators/languages/node/harness/run.sh | 46 +++++++++ .../validators/languages/node/runner.yaml | 3 + snippets/validators/languages/php/Dockerfile | 13 +++ .../validators/languages/php/harness/run.sh | 41 ++++++++ snippets/validators/languages/php/runner.yaml | 3 + snippets/validators/languages/ruby/Dockerfile | 8 ++ .../validators/languages/ruby/harness/run.sh | 46 +++++++++ .../validators/languages/ruby/runner.yaml | 3 + snippets/validators/languages/rust/Dockerfile | 8 ++ .../validators/languages/rust/harness/run.sh | 37 +++++++ .../validators/languages/rust/runner.yaml | 3 + 28 files changed, 572 insertions(+) create mode 100644 snippets/validators/languages/browser/Dockerfile create mode 100644 snippets/validators/languages/browser/harness/check.js create mode 100755 snippets/validators/languages/browser/harness/run.sh create mode 100644 snippets/validators/languages/browser/runner.yaml create mode 100644 snippets/validators/languages/dotnet-client/Dockerfile create mode 100755 snippets/validators/languages/dotnet-client/harness/run.sh create mode 100644 snippets/validators/languages/dotnet-client/runner.yaml create mode 100644 snippets/validators/languages/dotnet-server/Dockerfile create mode 100755 snippets/validators/languages/dotnet-server/harness/run.sh create mode 100644 snippets/validators/languages/dotnet-server/runner.yaml create mode 100644 snippets/validators/languages/go/Dockerfile create mode 100755 snippets/validators/languages/go/harness/run.sh create mode 100644 snippets/validators/languages/go/runner.yaml create mode 100644 snippets/validators/languages/jvm/Dockerfile create mode 100755 snippets/validators/languages/jvm/harness/run.sh create mode 100644 snippets/validators/languages/jvm/runner.yaml create mode 100644 snippets/validators/languages/node/Dockerfile create mode 100755 snippets/validators/languages/node/harness/run.sh create mode 100644 snippets/validators/languages/node/runner.yaml create mode 100644 snippets/validators/languages/php/Dockerfile create mode 100755 snippets/validators/languages/php/harness/run.sh create mode 100644 snippets/validators/languages/php/runner.yaml create mode 100644 snippets/validators/languages/ruby/Dockerfile create mode 100755 snippets/validators/languages/ruby/harness/run.sh create mode 100644 snippets/validators/languages/ruby/runner.yaml create mode 100644 snippets/validators/languages/rust/Dockerfile create mode 100755 snippets/validators/languages/rust/harness/run.sh create mode 100644 snippets/validators/languages/rust/runner.yaml diff --git a/snippets/validators/languages/browser/Dockerfile b/snippets/validators/languages/browser/Dockerfile new file mode 100644 index 0000000..f182dd2 --- /dev/null +++ b/snippets/validators/languages/browser/Dockerfile @@ -0,0 +1,13 @@ +# Playwright's official image bundles Chromium + headless Chrome + Node + +# Playwright. The npm install pins the matching JS-module version so the +# bundled browser binary matches the API we call against. +FROM mcr.microsoft.com/playwright:v1.59.1-noble + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/browser/harness /harness + +RUN cd /harness && npm install --silent --no-audit --no-fund --no-progress playwright@1.59.1 + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/browser/harness/check.js b/snippets/validators/languages/browser/harness/check.js new file mode 100644 index 0000000..115b8e8 --- /dev/null +++ b/snippets/validators/languages/browser/harness/check.js @@ -0,0 +1,44 @@ +// Loads the staged HTML in headless Chromium, polls the page text for the +// EXAM-HELLO success line, and exits 0 when matched. The browser is given +// up to 30 seconds to fetch the LaunchDarkly client SDK from its CDN, +// initialize, and evaluate the flag. +const { chromium } = require('/harness/node_modules/playwright'); + +(async () => { + const entrypoint = process.env.SNIPPET_ENTRYPOINT || 'index.html'; + const url = `file:///snippet/${entrypoint}`; + const successRe = /feature flag evaluates to true/i; + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Mirror page console messages to validator stdout so a snippet that + // fails to init is debuggable from the run log. + page.on('console', msg => console.log('[browser]', msg.text())); + page.on('pageerror', err => console.log('[browser:error]', err.message)); + + await page.goto(url); + + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + const text = (await page.textContent('body')) || ''; + if (successRe.test(text)) { + console.log(text.trim()); + console.log('validator: ok'); + await browser.close(); + process.exit(0); + } + await page.waitForTimeout(500); + } + + const finalText = (await page.textContent('body')) || ''; + console.error('validator: did not see expected line: feature flag evaluates to true'); + console.error('--- final body text ---'); + console.error(finalText.trim()); + await browser.close(); + process.exit(1); +})().catch(async (err) => { + console.error('validator: harness error:', err.message); + process.exit(1); +}); diff --git a/snippets/validators/languages/browser/harness/run.sh b/snippets/validators/languages/browser/harness/run.sh new file mode 100755 index 0000000..e396662 --- /dev/null +++ b/snippets/validators/languages/browser/harness/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Runs the staged HTML snippet in headless Chromium and watches the page +# text for the EXAM-HELLO success line. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_CLIENT_SIDE_ID LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +# The check.js harness reads SNIPPET_ENTRYPOINT and locates /snippet/. +# All output goes to stdout/stderr — match handling lives in check.js since +# the success criterion is the page DOM text, not the program log. +exec node /harness/check.js diff --git a/snippets/validators/languages/browser/runner.yaml b/snippets/validators/languages/browser/runner.yaml new file mode 100644 index 0000000..2cbdbf5 --- /dev/null +++ b/snippets/validators/languages/browser/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/browser-validator diff --git a/snippets/validators/languages/dotnet-client/Dockerfile b/snippets/validators/languages/dotnet-client/Dockerfile new file mode 100644 index 0000000..d4f0e63 --- /dev/null +++ b/snippets/validators/languages/dotnet-client/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/dotnet-client/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/dotnet-client/harness/run.sh b/snippets/validators/languages/dotnet-client/harness/run.sh new file mode 100755 index 0000000..b0f29d7 --- /dev/null +++ b/snippets/validators/languages/dotnet-client/harness/run.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Runs the staged .NET client snippet against a real LaunchDarkly environment. +# The snippet uses top-level statements (no namespace), so the synthesized +# .csproj is the bare minimum a `dotnet new console` would produce. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_MOBILE_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +cat > HelloDotNetClient.csproj <<'EOF' + + + Exe + net8.0 + enable + enable + + +EOF + +if [ -f requirements.txt ]; then + while IFS= read -r line; do + [ -z "$line" ] && continue + dotnet add package "$line" --no-restore >/dev/null + done < requirements.txt +fi + +dotnet restore --verbosity quiet >/dev/null 2>&1 || true + +LOG=$(mktemp) + +# .NET client snippet exits naturally after evaluating the flag (calls +# client.Dispose()), so we don't need CI=1 nor a long timeout. +timeout --signal=TERM 90s dotnet run --project . --verbosity quiet >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 80 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/dotnet-client/runner.yaml b/snippets/validators/languages/dotnet-client/runner.yaml new file mode 100644 index 0000000..6867bf8 --- /dev/null +++ b/snippets/validators/languages/dotnet-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/dotnet-client-validator diff --git a/snippets/validators/languages/dotnet-server/Dockerfile b/snippets/validators/languages/dotnet-server/Dockerfile new file mode 100644 index 0000000..debf5a1 --- /dev/null +++ b/snippets/validators/languages/dotnet-server/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/dotnet-server/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/dotnet-server/harness/run.sh b/snippets/validators/languages/dotnet-server/harness/run.sh new file mode 100755 index 0000000..0561e5a --- /dev/null +++ b/snippets/validators/languages/dotnet-server/harness/run.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Runs the staged .NET server snippet against a real LaunchDarkly environment. +# Synthesizes a minimal .csproj around the snippet's Program.cs and pulls +# whatever NuGet packages the snippet's `validation.requirements` lists. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +# .NET wants a project file; gonfalon's flow uses Visual Studio's "new +# console app" wizard which creates one. We synthesize the minimum. +cat > HelloDotNet.csproj <<'EOF' + + + Exe + net8.0 + disable + HelloDotNet + HelloDotNet + + +EOF + +if [ -f requirements.txt ]; then + while IFS= read -r line; do + [ -z "$line" ] && continue + # `dotnet add package` does not accept --verbosity; redirect noise to /dev/null. + dotnet add package "$line" --no-restore >/dev/null + done < requirements.txt +fi + +dotnet restore --verbosity quiet >/dev/null 2>&1 || true + +LOG=$(mktemp) + +CI=1 timeout --signal=TERM 180s dotnet run --project . --verbosity quiet >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 170 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/dotnet-server/runner.yaml b/snippets/validators/languages/dotnet-server/runner.yaml new file mode 100644 index 0000000..117c59e --- /dev/null +++ b/snippets/validators/languages/dotnet-server/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/dotnet-server-validator diff --git a/snippets/validators/languages/go/Dockerfile b/snippets/validators/languages/go/Dockerfile new file mode 100644 index 0000000..7f21abb --- /dev/null +++ b/snippets/validators/languages/go/Dockerfile @@ -0,0 +1,10 @@ +# Build context is `validators/`. See validators/languages/python/Dockerfile +# for the rationale. +FROM golang:1.24 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/go/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/go/harness/run.sh b/snippets/validators/languages/go/harness/run.sh new file mode 100755 index 0000000..d3b7694 --- /dev/null +++ b/snippets/validators/languages/go/harness/run.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Runs the staged Go snippet against a real LaunchDarkly environment. +# Inputs (env): LAUNCHDARKLY_SDK_KEY, LAUNCHDARKLY_FLAG_KEY, SNIPPET_ENTRYPOINT. +# +# The gonfalon `go mod init` step is reproduced here: we initialize a +# throwaway module in the working dir and let `go mod tidy` resolve the +# imports the snippet brings in. This mirrors what a developer following +# the Get Started instructions would do. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +# Stage the snippet contents into a writable workdir (the bind-mount is +# read-only). go mod init writes go.mod / go.sum, so we can't operate on +# /snippet directly. +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +go mod init example/hello-go >/dev/null 2>&1 +go mod tidy >/dev/null 2>&1 + +LOG=$(mktemp) + +# CI=1 makes the snippet exit after the first evaluation instead of blocking +# on the listener loop. +CI=1 go run "$SNIPPET_ENTRYPOINT" >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 60 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/go/runner.yaml b/snippets/validators/languages/go/runner.yaml new file mode 100644 index 0000000..b23424a --- /dev/null +++ b/snippets/validators/languages/go/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/go-validator diff --git a/snippets/validators/languages/jvm/Dockerfile b/snippets/validators/languages/jvm/Dockerfile new file mode 100644 index 0000000..00243ba --- /dev/null +++ b/snippets/validators/languages/jvm/Dockerfile @@ -0,0 +1,8 @@ +FROM maven:3.9-eclipse-temurin-17 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/jvm/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/jvm/harness/run.sh b/snippets/validators/languages/jvm/harness/run.sh new file mode 100755 index 0000000..ffb2261 --- /dev/null +++ b/snippets/validators/languages/jvm/harness/run.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Runs the staged Java snippet against a real LaunchDarkly environment. +# Synthesizes a complete pom.xml around the snippet's App.java rather +# than reproducing gonfalon's `mvn archetype:generate + manual fragment +# pasting` flow — a developer following the gonfalon instructions ends +# up with the same project shape, just authored manually. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +# The gonfalon snippet says "Remove the prepopulated lines except the +# first line" — the "first line" is `package com.launchdarkly.tutorial;` +# which `mvn archetype:generate` writes for the user. The snippet itself +# doesn't carry it (gonfalon's UI is showing only the body to type), so +# we add it back here so javac can resolve the mainClass declared in +# the synthesized pom.xml. +appfile="$SNIPPET_ENTRYPOINT" +if [ -f "$appfile" ] && ! head -1 "$appfile" | grep -q '^package '; then + tmp=$(mktemp) + printf 'package com.launchdarkly.tutorial;\n\n' > "$tmp" + cat "$appfile" >> "$tmp" + mv "$tmp" "$appfile" +fi + +cat > pom.xml <<'EOF' + + + 4.0.0 + com.launchdarkly.tutorial + hello-java + 1.0-SNAPSHOT + + 17 + 17 + UTF-8 + + + + com.launchdarkly + launchdarkly-java-server-sdk + + 7.13.4 + + + + + + maven-assembly-plugin + + + + com.launchdarkly.tutorial.App + + + + jar-with-dependencies + + + + + + +EOF + +LOG=$(mktemp) +BUILDLOG=$(mktemp) + +# Compile + assemble. We keep mvn output in BUILDLOG and only print it +# when the build fails, so a clean run is quiet. +if ! mvn -B -q clean compile assembly:single -DskipTests >"$BUILDLOG" 2>&1; then + echo "validator: maven build failed" >&2 + echo "--- mvn output ---" >&2 + cat "$BUILDLOG" >&2 + exit 1 +fi +rm -f "$BUILDLOG" + +CI=1 timeout --signal=TERM 60s java -jar "target/hello-java-1.0-SNAPSHOT-jar-with-dependencies.jar" >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 50 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/jvm/runner.yaml b/snippets/validators/languages/jvm/runner.yaml new file mode 100644 index 0000000..dd9c93a --- /dev/null +++ b/snippets/validators/languages/jvm/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/jvm-validator diff --git a/snippets/validators/languages/node/Dockerfile b/snippets/validators/languages/node/Dockerfile new file mode 100644 index 0000000..2604928 --- /dev/null +++ b/snippets/validators/languages/node/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/node/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/node/harness/run.sh b/snippets/validators/languages/node/harness/run.sh new file mode 100755 index 0000000..bad2dc9 --- /dev/null +++ b/snippets/validators/languages/node/harness/run.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Runs the staged Node.js snippet against a real LaunchDarkly environment. +# Inputs (env): LAUNCHDARKLY_SDK_KEY (or _CLIENT_SIDE_ID for client SDKs), +# LAUNCHDARKLY_FLAG_KEY, SNIPPET_ENTRYPOINT. +# +# The snippet's `validation.requirements` line(s) are passed in via a +# stage-time requirements.txt file (the dispatcher writes it). For Node, +# each line is treated as an `npm install ` argument. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +# Stage to a writable workdir; npm install writes node_modules. +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +# Initialize package.json if absent (snippet's `mkdir + npm init` step +# isn't reproduced verbatim — npm init is interactive). +if [ ! -f package.json ]; then + npm init -y >/dev/null +fi + +if [ -f requirements.txt ]; then + # Each non-empty line is an npm install target. + while IFS= read -r line; do + [ -z "$line" ] && continue + npm install --silent --no-audit --no-fund --no-progress "$line" + done < requirements.txt +fi + +LOG=$(mktemp) + +CI=1 timeout --signal=TERM 60s node "$SNIPPET_ENTRYPOINT" >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 50 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/node/runner.yaml b/snippets/validators/languages/node/runner.yaml new file mode 100644 index 0000000..e12f95d --- /dev/null +++ b/snippets/validators/languages/node/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/node-validator diff --git a/snippets/validators/languages/php/Dockerfile b/snippets/validators/languages/php/Dockerfile new file mode 100644 index 0000000..ac42d4f --- /dev/null +++ b/snippets/validators/languages/php/Dockerfile @@ -0,0 +1,13 @@ +FROM php:8.3-cli + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git unzip ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* \ + && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/php/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/php/harness/run.sh b/snippets/validators/languages/php/harness/run.sh new file mode 100755 index 0000000..6e56fdd --- /dev/null +++ b/snippets/validators/languages/php/harness/run.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Runs the staged PHP snippet against a real LaunchDarkly environment. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +# composer require ... per requirements line. The image bundles a +# system-wide `composer` binary, so we use it directly rather than +# bootstrapping composer.phar like the snippet does. +if [ -f requirements.txt ]; then + pkgs="" + while IFS= read -r line; do + [ -z "$line" ] && continue + pkgs="$pkgs $line" + done < requirements.txt + if [ -n "$pkgs" ]; then + # shellcheck disable=SC2086 + composer require --quiet --no-interaction --no-progress $pkgs + fi +fi + +LOG=$(mktemp) + +# PHP snippet loops forever with sleep(1); time out and SIGTERM after match. +timeout --signal=TERM 90s php -d output_buffering=Off "$SNIPPET_ENTRYPOINT" >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 80 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/php/runner.yaml b/snippets/validators/languages/php/runner.yaml new file mode 100644 index 0000000..bce9adf --- /dev/null +++ b/snippets/validators/languages/php/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/php-validator diff --git a/snippets/validators/languages/ruby/Dockerfile b/snippets/validators/languages/ruby/Dockerfile new file mode 100644 index 0000000..24e55d2 --- /dev/null +++ b/snippets/validators/languages/ruby/Dockerfile @@ -0,0 +1,8 @@ +FROM ruby:3.3 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/ruby/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/ruby/harness/run.sh b/snippets/validators/languages/ruby/harness/run.sh new file mode 100755 index 0000000..d110ed2 --- /dev/null +++ b/snippets/validators/languages/ruby/harness/run.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Runs the staged Ruby snippet against a real LaunchDarkly environment. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cp -r /snippet/. "$WORK/" +cd "$WORK" + +# Generate a Gemfile from validation.requirements (newline-separated gem names). +if [ -f requirements.txt ] && [ ! -f Gemfile ]; then + { + echo "source 'https://rubygems.org'" + while IFS= read -r line; do + [ -z "$line" ] && continue + echo "gem '$line'" + done < requirements.txt + } > Gemfile +fi + +if [ -f Gemfile ]; then + bundle install --quiet +fi + +LOG=$(mktemp) + +# Ruby snippet blocks on Thread sleep; we time it out and SIGTERM after match. +# Force line-buffered stdout — Ruby block-buffers when stdout isn't a tty, +# which would hide the success line until the deadline fires. stdbuf only +# affects libc-level buffering and doesn't help with Ruby's own IO layer, +# so we wrap the snippet in a one-liner that sets $stdout.sync = true +# before loading it. This keeps the snippet itself unmodified. +timeout --signal=TERM 90s ruby -e '$stdout.sync = true; load ENV["SNIPPET_ENTRYPOINT"]' >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 80 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/ruby/runner.yaml b/snippets/validators/languages/ruby/runner.yaml new file mode 100644 index 0000000..1a285ce --- /dev/null +++ b/snippets/validators/languages/ruby/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/ruby-validator diff --git a/snippets/validators/languages/rust/Dockerfile b/snippets/validators/languages/rust/Dockerfile new file mode 100644 index 0000000..3c1e413 --- /dev/null +++ b/snippets/validators/languages/rust/Dockerfile @@ -0,0 +1,8 @@ +FROM rust:1.83 + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/rust/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/rust/harness/run.sh b/snippets/validators/languages/rust/harness/run.sh new file mode 100755 index 0000000..3c88249 --- /dev/null +++ b/snippets/validators/languages/rust/harness/run.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Runs the staged Rust snippet against a real LaunchDarkly environment. +# The harness reproduces gonfalon's `cargo new` + `cargo add` flow: +# bootstrap a Cargo project, drop the snippet's src/main.rs over the +# default template, add the SDK + tokio dependencies, and run it. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cd "$WORK" + +cargo new --quiet --bin hello-rust +cd hello-rust + +# Replace the default src/main.rs with the snippet body. The snippet's +# `file:` is `src/main.rs`, so /snippet/src/main.rs holds it. +cp "/snippet/$SNIPPET_ENTRYPOINT" "$SNIPPET_ENTRYPOINT" + +cargo add --quiet launchdarkly-server-sdk +cargo add --quiet tokio@1 -F rt,macros + +LOG=$(mktemp) + +timeout --signal=TERM 300s cargo run --quiet >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 290 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/rust/runner.yaml b/snippets/validators/languages/rust/runner.yaml new file mode 100644 index 0000000..3eef475 --- /dev/null +++ b/snippets/validators/languages/rust/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/rust-validator From 4ea69f25820b81f8ab322dd59e5d8d0eac585eaa Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:57:39 -0700 Subject: [PATCH 03/19] ci(snippets): per-SDK validator matrix workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.github/workflows/snippets-validate.yml` runs `snippets validate --sdk=` for every validator-bearing SDK as a parallel matrix cell. fail-fast is disabled so every cell runs to completion. Each cell uses `launchdarkly/gh-actions/actions/verify-hello-app@ verify-hello-app-v2.0.1` — the same action the hello-* repos use — to assume the AWS role from `vars.AWS_ROLE_ARN` (a repo *variable*, not a secret) via OIDC, fetch the LaunchDarkly Sandbox account credential from Secrets Manager, and inject it as LAUNCHDARKLY_SDK_KEY / LAUNCHDARKLY_CLIENT_SIDE_ID / LAUNCHDARKLY_MOBILE_KEY based on each row's `key-type:` field (server | client | mobile). LAUNCHDARKLY_FLAG_KEY=sample-feature is exported in the `command:` block; the action only handles the SDK-side credential. No static LD secrets live on this repo. After the matrix finishes, a final `summary` job downloads each cell's status + log artifact, writes a markdown table to $GITHUB_STEP_SUMMARY listing each SDK / pass-or-fail / 3-line excerpt of the failure log, and exits non-zero if any cell failed so the PR check goes red. Initial matrix covers the eleven SDKs with validators landed in this PR (python from the parent branch + ten new). Additional rows land as more validators get wired (haskell, erlang, lua, cpp, ios, vue, android, flutter, react-native, react-client, dotnet-client once a real mobile key is provisioned). See validators/languages/ for the current list. --- .github/workflows/snippets-validate.yml | 164 ++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/snippets-validate.yml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml new file mode 100644 index 0000000..53bc66d --- /dev/null +++ b/.github/workflows/snippets-validate.yml @@ -0,0 +1,164 @@ +name: Validate snippets + +# Runs the per-SDK validators in parallel. Each cell delegates to +# `launchdarkly/gh-actions/actions/verify-hello-app@v2.0.1`, which is the +# same action the `hello-*` repos use to: +# - assume the configured AWS role via OIDC (`vars.AWS_ROLE_ARN`) +# - fetch the LaunchDarkly Sandbox account credentials from Secrets +# Manager and inject the appropriate key as an env var +# (LAUNCHDARKLY_SDK_KEY, LAUNCHDARKLY_CLIENT_SIDE_ID, or +# LAUNCHDARKLY_MOBILE_KEY based on the use_*_key flag) +# - run `command:` and assert its output contains the EXAM-HELLO +# `feature flag evaluates to true` line. +# +# Each matrix row declares `key-type: server | client | mobile` so the +# right `use_*_key` flag is set for the SDK under test. We always set +# LAUNCHDARKLY_FLAG_KEY=sample-feature ourselves; the action only handles +# the SDK/client/mobile key. +# +# fail-fast: false so every cell runs to completion. A final `summary` job +# aggregates per-cell artifacts into a markdown table. + +on: + pull_request: + paths: + - 'snippets/**' + - '.github/workflows/snippets-validate.yml' + workflow_dispatch: + +permissions: + id-token: write # OIDC for verify-hello-app's AWS role assumption + contents: read + +concurrency: + group: snippets-validate-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: ${{ matrix.sdk }} + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + include: + # T1 — Linux server / native runtimes that fit cleanly in Docker. + - { sdk: python-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: go-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: node-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: node-client-sdk, runs-on: ubuntu-latest, key-type: client } + - { sdk: ruby-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: php-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: rust-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: dotnet-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: dotnet-client-sdk, runs-on: ubuntu-latest, key-type: mobile } + - { sdk: java-server-sdk, runs-on: ubuntu-latest, key-type: server } + # T2 — browser SDKs (headless Playwright): + - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } + # Additional T1 SDKs append here as their snippet dirs land: + # erlang-server-sdk, haskell-server-sdk, + # lua-server-sdk, cpp-server-sdk, cpp-client-sdk. + # Additional T2 SDKs: + # react-client-sdk, react-client-sdk-legacy, vue-client-sdk + # (key-type: client) + # T3 — mobile/native non-Apple (Linux native runners): + # android-client-sdk (key-type: mobile) + # flutter-client-sdk (key-type: mobile) + # react-native-client-sdk (key-type: mobile) + # T4 — iOS (macos-latest, xcodebuild): + # - { sdk: ios-client-sdk, runs-on: macos-latest, key-type: mobile } + # Skipped: roku-client-sdk (validation: none, manual procedure). + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Validate + id: validate + uses: launchdarkly/gh-actions/actions/verify-hello-app@verify-hello-app-v2.0.1 + with: + use_server_key: ${{ matrix.key-type == 'server' }} + use_client_key: ${{ matrix.key-type == 'client' }} + use_mobile_key: ${{ matrix.key-type == 'mobile' }} + role_arn: ${{ vars.AWS_ROLE_ARN }} + command: | + set -o pipefail + export LAUNCHDARKLY_FLAG_KEY=sample-feature + cd snippets + go run ./cmd/snippets validate --sdk=${{ matrix.sdk }} \ + --sdks=./sdks --validators=./validators 2>&1 | tee /tmp/validate.log + + - name: Record result + if: always() + run: | + mkdir -p /tmp/result + if [ "${{ steps.validate.outcome }}" = "success" ]; then + echo "ok" > /tmp/result/status + else + echo "fail" > /tmp/result/status + fi + [ -f /tmp/validate.log ] && cp /tmp/validate.log /tmp/result/log + + - name: Upload result + if: always() + uses: actions/upload-artifact@v4 + with: + name: result-${{ matrix.sdk }} + path: /tmp/result/ + if-no-files-found: ignore + retention-days: 14 + + summary: + name: Validation summary + if: always() + needs: validate + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: results + pattern: result-* + + - name: Build markdown table + run: | + { + echo "## Snippet validation summary" + echo + echo "Run: \`${{ github.run_id }}\` · Commit: \`${{ github.sha }}\`" + echo + echo "| SDK | Result | Excerpt |" + echo "|---|---|---|" + for d in results/result-*; do + [ -d "$d" ] || continue + sdk=$(basename "$d" | sed 's/^result-//') + status="unknown" + [ -f "$d/status" ] && status=$(cat "$d/status") + if [ "$status" = "ok" ]; then + echo "| \`$sdk\` | ok | |" + else + excerpt="" + if [ -f "$d/log" ]; then + excerpt=$(tail -n 50 "$d/log" \ + | grep -v '^$' \ + | tail -n 3 \ + | sed -e 's/|/\\|/g' -e 's/`/\\`/g' \ + | tr '\n' ' ') + fi + echo "| \`$sdk\` | **FAIL** | $excerpt |" + fi + done + echo + echo "Per-job logs are uploaded as \`result-\` artifacts (14-day retention)." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail summary if any cell failed + run: | + for d in results/result-*; do + [ -d "$d" ] || continue + if [ -f "$d/status" ] && [ "$(cat "$d/status")" != "ok" ]; then + echo "At least one validator failed; see step summary above." >&2 + exit 1 + fi + done From 9822d08964e6652339268f6b40383cde34ef31db Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:17:06 -0700 Subject: [PATCH 04/19] ci(snippets): use vars.AWS_ROLE_ARN_EXAMPLES for the role ARN The repo variable was provisioned as AWS_ROLE_ARN_EXAMPLES (scoped to the examples sandbox) rather than the generic AWS_ROLE_ARN the hello-* repos use. Update both the role_arn input and the header comment. --- .github/workflows/snippets-validate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 53bc66d..3451c66 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -3,7 +3,7 @@ name: Validate snippets # Runs the per-SDK validators in parallel. Each cell delegates to # `launchdarkly/gh-actions/actions/verify-hello-app@v2.0.1`, which is the # same action the `hello-*` repos use to: -# - assume the configured AWS role via OIDC (`vars.AWS_ROLE_ARN`) +# - assume the configured AWS role via OIDC (`vars.AWS_ROLE_ARN_EXAMPLES`) # - fetch the LaunchDarkly Sandbox account credentials from Secrets # Manager and inject the appropriate key as an env var # (LAUNCHDARKLY_SDK_KEY, LAUNCHDARKLY_CLIENT_SIDE_ID, or @@ -82,7 +82,7 @@ jobs: use_server_key: ${{ matrix.key-type == 'server' }} use_client_key: ${{ matrix.key-type == 'client' }} use_mobile_key: ${{ matrix.key-type == 'mobile' }} - role_arn: ${{ vars.AWS_ROLE_ARN }} + role_arn: ${{ vars.AWS_ROLE_ARN_EXAMPLES }} command: | set -o pipefail export LAUNCHDARKLY_FLAG_KEY=sample-feature From 18ee8cb063cae549035cec030bed262557bcef55 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:20:38 -0700 Subject: [PATCH 05/19] ci(snippets): rely on verify-hello-app to inject LAUNCHDARKLY_FLAG_KEY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verify-hello-app action already pulls LAUNCHDARKLY_FLAG_KEY from the SSM parameter /sdk/common/hello-apps/boolean-flag-key — that's how every hello-* repo's CI gets the flag key without setting it in its own workflow. Our `export LAUNCHDARKLY_FLAG_KEY=sample-feature` in the command block was redundant and would silently override the shared value if it ever changes (e.g. if the test environment swaps the canonical flag key). Drop it; let the action manage it. --- .github/workflows/snippets-validate.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 3451c66..57c927f 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -12,9 +12,10 @@ name: Validate snippets # `feature flag evaluates to true` line. # # Each matrix row declares `key-type: server | client | mobile` so the -# right `use_*_key` flag is set for the SDK under test. We always set -# LAUNCHDARKLY_FLAG_KEY=sample-feature ourselves; the action only handles -# the SDK/client/mobile key. +# right `use_*_key` flag is set for the SDK under test. The action also +# injects LAUNCHDARKLY_FLAG_KEY from the SSM parameter +# /sdk/common/hello-apps/boolean-flag-key, matching what every hello-* +# repo's CI relies on; we don't set it ourselves. # # fail-fast: false so every cell runs to completion. A final `summary` job # aggregates per-cell artifacts into a markdown table. @@ -85,7 +86,6 @@ jobs: role_arn: ${{ vars.AWS_ROLE_ARN_EXAMPLES }} command: | set -o pipefail - export LAUNCHDARKLY_FLAG_KEY=sample-feature cd snippets go run ./cmd/snippets validate --sdk=${{ matrix.sdk }} \ --sdks=./sdks --validators=./validators 2>&1 | tee /tmp/validate.log From e368f8fa8c48fe898c144f7a0cfe6dd3d8c275f5 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:27:10 -0700 Subject: [PATCH 06/19] fix(snippets): align failing CI cells with the hello-app conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from the first CI run on the validator matrix: - rust validator: bump rust:1.83 → rust:1.85 base image. The transitive dep `time-macros 0.2.27` requires the `edition2024` cargo feature, only stabilized in 1.85. - js-client-sdk: context key was `'example-context-key'`. Every other hello-app uses `'example-user-key'`, and the test environment's `hello-boolean` flag is configured to evaluate to true for that user. The mismatched key was a snippet-author idiosyncrasy in gonfalon, not a deliberate divergence — normalize. - dotnet-client-sdk: same root cause — `Context.New("context-key- 123abc")` swapped for `Context.New("example-user-key")`. Validator observed the flag evaluating to False against `context-key-123abc` in the shared test env. The other 8 cells (java, python, dotnet-server, php, node-client, go, node-server, ruby) all green on this run. --- .../snippets/getting-started/program-cs.snippet.md | 2 +- .../snippets/getting-started/index-html.snippet.md | 2 +- snippets/validators/languages/rust/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md index 38900b6..1124862 100644 --- a/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md +++ b/snippets/sdks/dotnet-client-sdk/snippets/getting-started/program-cs.snippet.md @@ -25,7 +25,7 @@ Open the file `Program.cs` and add the following code: using LaunchDarkly.Sdk; using LaunchDarkly.Sdk.Client; -var context = Context.New("context-key-123abc"); +var context = Context.New("example-user-key"); var timeSpan = TimeSpan.FromSeconds(10); var client = LdClient.Init( Configuration.Default("{{ mobileKey }}", ConfigurationBuilder.AutoEnvAttributes.Enabled), diff --git a/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md b/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md index 185e44e..8e29703 100644 --- a/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md +++ b/snippets/sdks/js-client-sdk/snippets/getting-started/index-html.snippet.md @@ -51,7 +51,7 @@ Create a file called `index.html` and add the following code: // LaunchDarkly contexts dashboard soon after you run the demo. const context = { kind: 'user', - key: 'example-context-key', + key: 'example-user-key', name: 'Sandy' }; diff --git a/snippets/validators/languages/rust/Dockerfile b/snippets/validators/languages/rust/Dockerfile index 3c1e413..9da7958 100644 --- a/snippets/validators/languages/rust/Dockerfile +++ b/snippets/validators/languages/rust/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.83 +FROM rust:1.85 WORKDIR /work From 8e1600864b53a5ea971a3589966603cc325fba84 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:32:06 -0700 Subject: [PATCH 07/19] ci(snippets): drop T1/T2/T3/T4 labels from the matrix comments LaunchDarkly already uses 'tier' to classify SDKs (the SDK tier list); re-using T1/T2/T3/T4 in our validator matrix to mean 'Linux Docker' vs 'browser' vs 'mobile' vs 'macOS' creates a name collision that will mislead anyone skimming the workflow. Group the matrix rows by plain runtime descriptor instead. --- .github/workflows/snippets-validate.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 57c927f..8ce2c34 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -43,7 +43,7 @@ jobs: fail-fast: false matrix: include: - # T1 — Linux server / native runtimes that fit cleanly in Docker. + # Linux server-runtime SDKs (Docker validators): - { sdk: python-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: go-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: node-server-sdk, runs-on: ubuntu-latest, key-type: server } @@ -54,20 +54,17 @@ jobs: - { sdk: dotnet-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: dotnet-client-sdk, runs-on: ubuntu-latest, key-type: mobile } - { sdk: java-server-sdk, runs-on: ubuntu-latest, key-type: server } - # T2 — browser SDKs (headless Playwright): + # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } - # Additional T1 SDKs append here as their snippet dirs land: - # erlang-server-sdk, haskell-server-sdk, - # lua-server-sdk, cpp-server-sdk, cpp-client-sdk. - # Additional T2 SDKs: - # react-client-sdk, react-client-sdk-legacy, vue-client-sdk - # (key-type: client) - # T3 — mobile/native non-Apple (Linux native runners): - # android-client-sdk (key-type: mobile) - # flutter-client-sdk (key-type: mobile) - # react-native-client-sdk (key-type: mobile) - # T4 — iOS (macos-latest, xcodebuild): - # - { sdk: ios-client-sdk, runs-on: macos-latest, key-type: mobile } + # + # Validators not yet wired (snippets ported, harness pending): + # server-runtime: erlang-server-sdk, haskell-server-sdk, + # lua-server-sdk, cpp-server-sdk, cpp-client-sdk + # browser: react-client-sdk, react-client-sdk-legacy, + # vue-client-sdk (key-type: client) + # Linux mobile/native: android-client-sdk, flutter-client-sdk, + # react-native-client-sdk (key-type: mobile) + # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) # Skipped: roku-client-sdk (validation: none, manual procedure). steps: - uses: actions/checkout@v4 From 5b65de191ab70f5138e34062112980e1a17c0c56 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:59:30 -0700 Subject: [PATCH 08/19] feat(snippets): cpp-server-sdk validator + canonical print line Pre-clones cpp-sdks at launchdarkly-cpp-server-v3.10.1 and prewarms ccache by building the SDK once at image-build time, so per-validate cycles only compile the user's main.cpp. Snippet's print line was the legacy "Feature flag '' is true" pattern; updated to the EXAM-HELLO canonical form so await_success_line matches. --- .github/workflows/snippets-validate.yml | 3 +- .../getting-started/main-cpp.snippet.md | 10 ++-- .../languages/cpp-server/Dockerfile | 42 +++++++++++++++ .../languages/cpp-server/harness/run.sh | 51 +++++++++++++++++++ .../languages/cpp-server/runner.yaml | 3 ++ 5 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 snippets/validators/languages/cpp-server/Dockerfile create mode 100755 snippets/validators/languages/cpp-server/harness/run.sh create mode 100644 snippets/validators/languages/cpp-server/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 8ce2c34..d1b4f28 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -54,12 +54,13 @@ jobs: - { sdk: dotnet-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: dotnet-client-sdk, runs-on: ubuntu-latest, key-type: mobile } - { sdk: java-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: cpp-server-sdk, runs-on: ubuntu-latest, key-type: server } # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): # server-runtime: erlang-server-sdk, haskell-server-sdk, - # lua-server-sdk, cpp-server-sdk, cpp-client-sdk + # lua-server-sdk, cpp-client-sdk # browser: react-client-sdk, react-client-sdk-legacy, # vue-client-sdk (key-type: client) # Linux mobile/native: android-client-sdk, flutter-client-sdk, diff --git a/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md b/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md index dd9b128..350ca09 100644 --- a/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md +++ b/snippets/sdks/cpp-server-sdk/snippets/getting-started/main-cpp.snippet.md @@ -14,9 +14,9 @@ inputs: description: Default flag key baked into the rendered source. ld-application: slot: main-cpp -# Validator pending. Per-validate cycle requires a Docker image with -# cmake + boost + openssl + ninja and a checkout of cpp-sdks; first -# build is multi-minute even with prebuilt deps. +validation: + runtime: cpp-server + entrypoint: main.cpp --- Create a file named `main.cpp` add the following code: @@ -68,8 +68,8 @@ int main() { bool const flag_value = client.BoolVariation(context, "{{ featureKey }}", false); - std::cout << "*** Feature flag '{{ featureKey }}' is " - << (flag_value ? "true" : "false") << std::endl; + std::cout << "*** The '{{ featureKey }}' feature flag evaluates to " + << (flag_value ? "true" : "false") << "." << std::endl; return 0; } diff --git a/snippets/validators/languages/cpp-server/Dockerfile b/snippets/validators/languages/cpp-server/Dockerfile new file mode 100644 index 0000000..594ef4f --- /dev/null +++ b/snippets/validators/languages/cpp-server/Dockerfile @@ -0,0 +1,42 @@ +FROM ubuntu:24.04 + +# Build chain for the LD C++ Server SDK (cmake, ninja, boost, openssl, +# git for cloning cpp-sdks, ccache to keep per-validate rebuilds cheap). +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build pkg-config \ + libboost-all-dev libssl-dev \ + git ca-certificates ccache \ + && rm -rf /var/lib/apt/lists/* + +ENV CCACHE_DIR=/root/.ccache +ENV PATH="/usr/lib/ccache:${PATH}" + +# Pre-clone cpp-sdks. The snippet's CMakeLists references this via +# `add_subdirectory(cpp-sdks)` from the project root, so we symlink it +# into the staging dir at validate time rather than committing the +# tree to /work directly. +ARG CPP_SDKS_REF=launchdarkly-cpp-server-v3.10.1 +RUN git clone --depth 1 --branch "${CPP_SDKS_REF}" \ + https://github.com/launchdarkly/cpp-sdks.git /opt/cpp-sdks + +# Warm the ccache by configuring + building the server SDK once. Subsequent +# per-snippet builds compile only the user's main.cpp and link against the +# already-cached objects. +RUN mkdir -p /tmp/prewarm && cd /tmp/prewarm \ + && ln -s /opt/cpp-sdks cpp-sdks \ + && printf 'cmake_minimum_required(VERSION 3.19)\n\ +project(prewarm LANGUAGES CXX)\n\ +set(THREADS_PREFER_PTHREAD_FLAG ON)\n\ +find_package(Threads REQUIRED)\n\ +add_subdirectory(cpp-sdks)\n' > CMakeLists.txt \ + && mkdir build && cd build \ + && cmake -G Ninja -DBUILD_TESTING=OFF .. \ + && cmake --build . --target launchdarkly-cpp-server \ + && cd / && rm -rf /tmp/prewarm + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/cpp-server/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/cpp-server/harness/run.sh b/snippets/validators/languages/cpp-server/harness/run.sh new file mode 100755 index 0000000..820aa3b --- /dev/null +++ b/snippets/validators/languages/cpp-server/harness/run.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Runs the staged C++ server snippet against a real LaunchDarkly environment. +# The snippet is just a single main.cpp; gonfalon's Get Started flow has the +# user clone cpp-sdks alongside their project and add it via CMake. We mirror +# that here: the Dockerfile pre-cloned cpp-sdks at /opt/cpp-sdks and prewarmed +# the build cache, so per-validate cycles only compile the user's main.cpp +# and link against the cached SDK objects. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cd "$WORK" + +cp "/snippet/$SNIPPET_ENTRYPOINT" main.cpp +ln -s /opt/cpp-sdks cpp-sdks + +cat > CMakeLists.txt <<'EOF' +cmake_minimum_required(VERSION 3.19) +project(hello-cpp-server LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) +add_subdirectory(cpp-sdks) +add_executable(hello main.cpp) +target_link_libraries(hello PRIVATE launchdarkly::server Threads::Threads) +EOF + +mkdir build +cd build +cmake -G Ninja -DBUILD_TESTING=OFF .. >/tmp/cmake.log 2>&1 \ + || { cat /tmp/cmake.log >&2; exit 1; } +cmake --build . --target hello >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +LOG=$(mktemp) + +timeout --signal=TERM 60s ./hello >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 55 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/cpp-server/runner.yaml b/snippets/validators/languages/cpp-server/runner.yaml new file mode 100644 index 0000000..b5c0a21 --- /dev/null +++ b/snippets/validators/languages/cpp-server/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/cpp-server-validator From 220b916ec44f9ec608bd61553d2b7c44ce4f8522 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:04:08 -0700 Subject: [PATCH 09/19] feat(snippets): cpp-client-sdk validator + canonical print line Same shape as the cpp-server validator: pre-clones cpp-sdks at launchdarkly-cpp-client-v3.11.1, prewarms ccache by building the client-SDK target once. Snippet's print line was the legacy "Feature flag '' is true" pattern; updated to the EXAM-HELLO canonical. --- .github/workflows/snippets-validate.yml | 3 +- .../getting-started/main-cpp.snippet.md | 8 +-- .../languages/cpp-client/Dockerfile | 42 ++++++++++++++++ .../languages/cpp-client/harness/run.sh | 50 +++++++++++++++++++ .../languages/cpp-client/runner.yaml | 3 ++ 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 snippets/validators/languages/cpp-client/Dockerfile create mode 100755 snippets/validators/languages/cpp-client/harness/run.sh create mode 100644 snippets/validators/languages/cpp-client/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index d1b4f28..7f10908 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -55,12 +55,13 @@ jobs: - { sdk: dotnet-client-sdk, runs-on: ubuntu-latest, key-type: mobile } - { sdk: java-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: cpp-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: cpp-client-sdk, runs-on: ubuntu-latest, key-type: mobile } # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): # server-runtime: erlang-server-sdk, haskell-server-sdk, - # lua-server-sdk, cpp-client-sdk + # lua-server-sdk # browser: react-client-sdk, react-client-sdk-legacy, # vue-client-sdk (key-type: client) # Linux mobile/native: android-client-sdk, flutter-client-sdk, diff --git a/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md b/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md index 0b74fe5..e2662b1 100644 --- a/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md @@ -14,7 +14,9 @@ inputs: description: Default flag key baked into the rendered source. ld-application: slot: main-cpp -# Validator pending — same toolchain story as cpp-server-sdk. +validation: + runtime: cpp-client + entrypoint: main.cpp --- Create a file named `main.cpp` add the following code: @@ -65,8 +67,8 @@ int main() { bool const flag_value = client.BoolVariation("{{ featureKey }}", false); - std::cout << "*** Feature flag '{{ featureKey }}' is " - << (flag_value ? "true" : "false") << std::endl; + std::cout << "*** The '{{ featureKey }}' feature flag evaluates to " + << (flag_value ? "true" : "false") << "." << std::endl; return 0; } diff --git a/snippets/validators/languages/cpp-client/Dockerfile b/snippets/validators/languages/cpp-client/Dockerfile new file mode 100644 index 0000000..5788049 --- /dev/null +++ b/snippets/validators/languages/cpp-client/Dockerfile @@ -0,0 +1,42 @@ +FROM ubuntu:24.04 + +# Build chain for the LD C++ Client SDK (cmake, ninja, boost, openssl, +# git for cloning cpp-sdks, ccache to keep per-validate rebuilds cheap). +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build pkg-config \ + libboost-all-dev libssl-dev \ + git ca-certificates ccache \ + && rm -rf /var/lib/apt/lists/* + +ENV CCACHE_DIR=/root/.ccache +ENV PATH="/usr/lib/ccache:${PATH}" + +# Pre-clone cpp-sdks. The snippet's CMakeLists references this via +# `add_subdirectory(cpp-sdks)` from the project root, so we symlink it +# into the staging dir at validate time rather than committing the +# tree to /work directly. +ARG CPP_SDKS_REF=launchdarkly-cpp-client-v3.11.1 +RUN git clone --depth 1 --branch "${CPP_SDKS_REF}" \ + https://github.com/launchdarkly/cpp-sdks.git /opt/cpp-sdks + +# Warm the ccache by configuring + building the client SDK once. Subsequent +# per-snippet builds compile only the user's main.cpp and link against the +# already-cached objects. +RUN mkdir -p /tmp/prewarm && cd /tmp/prewarm \ + && ln -s /opt/cpp-sdks cpp-sdks \ + && printf 'cmake_minimum_required(VERSION 3.19)\n\ +project(prewarm LANGUAGES CXX)\n\ +set(THREADS_PREFER_PTHREAD_FLAG ON)\n\ +find_package(Threads REQUIRED)\n\ +add_subdirectory(cpp-sdks)\n' > CMakeLists.txt \ + && mkdir build && cd build \ + && cmake -G Ninja -DBUILD_TESTING=OFF .. \ + && cmake --build . --target launchdarkly-cpp-client \ + && cd / && rm -rf /tmp/prewarm + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/cpp-client/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/cpp-client/harness/run.sh b/snippets/validators/languages/cpp-client/harness/run.sh new file mode 100755 index 0000000..87e2694 --- /dev/null +++ b/snippets/validators/languages/cpp-client/harness/run.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Runs the staged C++ client snippet against a real LaunchDarkly environment. +# Mirrors gonfalon's Get Started flow: clone cpp-sdks alongside main.cpp, +# add it via CMake, link the client SDK target. The Dockerfile pre-cloned +# cpp-sdks at /opt/cpp-sdks and prewarmed the build cache, so per-validate +# cycles only compile the user's main.cpp. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_MOBILE_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +cd "$WORK" + +cp "/snippet/$SNIPPET_ENTRYPOINT" main.cpp +ln -s /opt/cpp-sdks cpp-sdks + +cat > CMakeLists.txt <<'EOF' +cmake_minimum_required(VERSION 3.19) +project(hello-cpp-client LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) +add_subdirectory(cpp-sdks) +add_executable(hello main.cpp) +target_link_libraries(hello PRIVATE launchdarkly::client Threads::Threads) +EOF + +mkdir build +cd build +cmake -G Ninja -DBUILD_TESTING=OFF .. >/tmp/cmake.log 2>&1 \ + || { cat /tmp/cmake.log >&2; exit 1; } +cmake --build . --target hello >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +LOG=$(mktemp) + +timeout --signal=TERM 60s ./hello >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 55 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/cpp-client/runner.yaml b/snippets/validators/languages/cpp-client/runner.yaml new file mode 100644 index 0000000..74e722d --- /dev/null +++ b/snippets/validators/languages/cpp-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/cpp-client-validator From e3155357d336c52523b9e64f1b459d45becd4338 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:10:29 -0700 Subject: [PATCH 10/19] feat(snippets): lua-server-sdk validator + canonical print line The Lua SDK is a thin wrapper around the C++ Server SDK's C binding, so the validator image builds cpp-sdks (launchdarkly-cpp-server-v3.10.1) as a shared library at image-build time and luarocks-installs the wrapper against it. Per-validate is just `lua hello.lua` against the staged snippet. Snippet's print line was the legacy "Feature flag '' is " pattern; updated to EXAM-HELLO canonical. --- .github/workflows/snippets-validate.yml | 4 +- .../getting-started/hello-lua.snippet.md | 8 ++-- .../languages/lua-server/Dockerfile | 45 +++++++++++++++++++ .../languages/lua-server/harness/run.sh | 24 ++++++++++ .../languages/lua-server/runner.yaml | 3 ++ 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 snippets/validators/languages/lua-server/Dockerfile create mode 100755 snippets/validators/languages/lua-server/harness/run.sh create mode 100644 snippets/validators/languages/lua-server/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 7f10908..cc73042 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -56,12 +56,12 @@ jobs: - { sdk: java-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: cpp-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: cpp-client-sdk, runs-on: ubuntu-latest, key-type: mobile } + - { sdk: lua-server-sdk, runs-on: ubuntu-latest, key-type: server } # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): - # server-runtime: erlang-server-sdk, haskell-server-sdk, - # lua-server-sdk + # server-runtime: erlang-server-sdk, haskell-server-sdk # browser: react-client-sdk, react-client-sdk-legacy, # vue-client-sdk (key-type: client) # Linux mobile/native: android-client-sdk, flutter-client-sdk, diff --git a/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md b/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md index e152ab8..e3c3285 100644 --- a/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md +++ b/snippets/sdks/lua-server-sdk/snippets/getting-started/hello-lua.snippet.md @@ -14,9 +14,9 @@ inputs: description: Default flag key baked into the rendered source. ld-application: slot: hello-lua -# Validator pending. Lua wraps the C++ Server SDK and requires the -# C++ build (cmake + boost + openssl + ninja) plus luarocks. A -# specialized validator image is needed; deferred. +validation: + runtime: lua-server + entrypoint: hello.lua --- Create a file named `hello.lua` and add the following code: @@ -35,5 +35,5 @@ local user = ld.makeContext({ }) local value = client:boolVariation(user, "{{ featureKey }}", false) -print("Feature flag '{{ featureKey }}' is "..tostring(value).."") +print("*** The '{{ featureKey }}' feature flag evaluates to "..tostring(value)..".") ``` diff --git a/snippets/validators/languages/lua-server/Dockerfile b/snippets/validators/languages/lua-server/Dockerfile new file mode 100644 index 0000000..8ff7475 --- /dev/null +++ b/snippets/validators/languages/lua-server/Dockerfile @@ -0,0 +1,45 @@ +FROM ubuntu:24.04 + +# The Lua server SDK is a thin Lua wrapper over the C++ Server SDK's C +# binding, so the validator image needs the C++ toolchain + the SDK's +# transitive deps + lua + luarocks. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build pkg-config \ + libboost-all-dev libssl-dev \ + lua5.3 liblua5.3-dev luarocks \ + git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Build the C++ Server SDK as a shared library and install to a known +# prefix; the lua rockspec's external_dependencies look up the C header +# and shared lib from $LD_DIR/include and $LD_DIR/lib respectively. +ARG CPP_SDKS_REF=launchdarkly-cpp-server-v3.10.1 +RUN git clone --depth 1 --branch "${CPP_SDKS_REF}" \ + https://github.com/launchdarkly/cpp-sdks.git /opt/cpp-sdks \ + && mkdir /opt/cpp-sdks/build && cd /opt/cpp-sdks/build \ + && cmake -G Ninja \ + -DBUILD_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DLD_BUILD_SHARED_LIBS=On \ + -DCMAKE_INSTALL_PREFIX=/opt/cpp-sdks/install .. \ + && cmake --build . --target launchdarkly-cpp-server \ + && cmake --install . \ + && cd / && rm -rf /opt/cpp-sdks/build /opt/cpp-sdks/.git + +ENV LD_LIBRARY_PATH=/opt/cpp-sdks/install/lib + +# Install the Lua server SDK rock against the built C++ shared lib. +ARG LUA_SDK_VERSION=2.1.3-0 +RUN luarocks install --tree=/opt/lua-rocks \ + launchdarkly-server-sdk ${LUA_SDK_VERSION} \ + LD_DIR=/opt/cpp-sdks/install + +ENV LUA_PATH="/opt/lua-rocks/share/lua/5.3/?.lua;/opt/lua-rocks/share/lua/5.3/?/init.lua;;" +ENV LUA_CPATH="/opt/lua-rocks/lib/lua/5.3/?.so;;" + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/lua-server/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/lua-server/harness/run.sh b/snippets/validators/languages/lua-server/harness/run.sh new file mode 100755 index 0000000..0ab100d --- /dev/null +++ b/snippets/validators/languages/lua-server/harness/run.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Runs the staged Lua snippet against a real LaunchDarkly environment. +# The Dockerfile pre-built the C++ Server SDK shared lib, installed the +# Lua wrapper rock against it, and pinned LUA_PATH/LUA_CPATH/LD_LIBRARY_PATH +# so `lua` finds the launchdarkly_server_sdk module without any per-validate +# setup. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +LOG=$(mktemp) + +timeout --signal=TERM 60s lua5.3 "/snippet/$SNIPPET_ENTRYPOINT" >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 55 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/lua-server/runner.yaml b/snippets/validators/languages/lua-server/runner.yaml new file mode 100644 index 0000000..b10379b --- /dev/null +++ b/snippets/validators/languages/lua-server/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/lua-server-validator From 14dda8ff447035466a08547558ecd7a578962f81 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:24:13 -0700 Subject: [PATCH 11/19] feat(snippets): haskell-server-sdk validator + LAUNCHDARKLY_FLAG_KEY fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a long-standing bug in the snippet (carried over from gonfalon): the program called lookupEnv "{{ featureKey }}", which renders as lookupEnv "sample-feature" — looking up an env var literally named after the flag, which always failed at runtime. The Nothing branch then defaulted back to "sample-feature", so the program worked by coincidence. Hello-haskell-server uses lookupEnv "LAUNCHDARKLY_FLAG_KEY"; aligning the snippet with the same pattern. Validator uses haskell:9.6.7-bullseye with a pre-compiled cabal project pinning launchdarkly-server-sdk-4.5.1 (latest on Hackage). The snippet's stack.yaml/package.yaml fragments are still authored for ld-application rendering but aren't exercised by the validator — the equivalent cabal project is baked into the image. --- .github/workflows/snippets-validate.yml | 3 +- .../getting-started/main-hs.snippet.md | 8 ++-- .../languages/haskell-server/Dockerfile | 44 +++++++++++++++++++ .../languages/haskell-server/harness/run.sh | 29 ++++++++++++ .../languages/haskell-server/runner.yaml | 3 ++ 5 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 snippets/validators/languages/haskell-server/Dockerfile create mode 100755 snippets/validators/languages/haskell-server/harness/run.sh create mode 100644 snippets/validators/languages/haskell-server/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index cc73042..cc9e4f2 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -57,11 +57,12 @@ jobs: - { sdk: cpp-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: cpp-client-sdk, runs-on: ubuntu-latest, key-type: mobile } - { sdk: lua-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: haskell-server-sdk, runs-on: ubuntu-latest, key-type: server } # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): - # server-runtime: erlang-server-sdk, haskell-server-sdk + # server-runtime: erlang-server-sdk # browser: react-client-sdk, react-client-sdk-legacy, # vue-client-sdk (key-type: client) # Linux mobile/native: android-client-sdk, flutter-client-sdk, diff --git a/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md b/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md index 6b624d2..eab8392 100644 --- a/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md +++ b/snippets/sdks/haskell-server-sdk/snippets/getting-started/main-hs.snippet.md @@ -11,9 +11,9 @@ inputs: description: Default flag key baked into the rendered source. Gonfalon's snippet uses this as the env-var name passed to lookupEnv (see comment below); validation reads LAUNCHDARKLY_FLAG_KEY at runtime. ld-application: slot: main-hs -# validation runtime not yet wired — Haskell stack-build harness pending. -# When added, the stack image needs launchdarkly-server-sdk + text -# pre-installed to keep per-validate cost reasonable. +validation: + runtime: haskell-server + entrypoint: app/Main.hs --- Edit `app/Main.hs` by adding the following code: @@ -98,6 +98,6 @@ main = do -- Set sdkKey to your LaunchDarkly SDK key. sdkKey <- lookupEnv "LAUNCHDARKLY_SDK_KEY" -- Set featureFlagKey to the feature flag key you want to evaluate. - featureFlagKey <- lookupEnv "{{ featureKey }}" + featureFlagKey <- lookupEnv "LAUNCHDARKLY_FLAG_KEY" evaluate sdkKey featureFlagKey ``` diff --git a/snippets/validators/languages/haskell-server/Dockerfile b/snippets/validators/languages/haskell-server/Dockerfile new file mode 100644 index 0000000..3df1009 --- /dev/null +++ b/snippets/validators/languages/haskell-server/Dockerfile @@ -0,0 +1,44 @@ +FROM haskell:9.6.7-bullseye + +# launchdarkly-server-sdk's transitive deps need libpcre + zlib + openssl. +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpcre3-dev zlib1g-dev libssl-dev \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Pre-build a cabal project mirroring what the snippet's `stack new` flow +# produces, so per-validate cycles only recompile the user's Main.hs. +# The snippet's stack.yaml/package.yaml fragments aren't exercised by the +# validator (they're just rendered into ld-application's get-started +# instructions); we build the equivalent cabal project here directly. +ARG LD_HASKELL_VERSION=4.5.1 +RUN mkdir -p /opt/hello-haskell/app +WORKDIR /opt/hello-haskell + +RUN printf 'cabal-version: 2.2\n\ +name: hello-haskell\n\ +version: 0.1.0.0\n\ +\n\ +executable hello-haskell-exe\n\ + main-is: Main.hs\n\ + hs-source-dirs: app\n\ + build-depends:\n\ + base >= 4.7 && < 5,\n\ + text,\n\ + launchdarkly-server-sdk == %s\n\ + default-language: Haskell2010\n\ + ghc-options: -threaded -rtsopts -with-rtsopts=-N\n' \ + "${LD_HASKELL_VERSION}" > hello-haskell.cabal + +# Placeholder Main.hs — replaced per-validate. We need *some* file here +# so `cabal build` succeeds and primes the dependency cache. +RUN printf 'module Main where\nmain :: IO ()\nmain = putStrLn "placeholder"\n' > app/Main.hs + +RUN cabal update && cabal build --only-dependencies && cabal build + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/haskell-server/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/haskell-server/harness/run.sh b/snippets/validators/languages/haskell-server/harness/run.sh new file mode 100755 index 0000000..3008222 --- /dev/null +++ b/snippets/validators/languages/haskell-server/harness/run.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Runs the staged Haskell snippet against a real LaunchDarkly environment. +# The Dockerfile pre-bootstrapped a cabal project at /opt/hello-haskell +# with launchdarkly-server-sdk + text already compiled. Per-validate just +# swaps in the user's Main.hs and does an incremental rebuild. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +cp "/snippet/$SNIPPET_ENTRYPOINT" /opt/hello-haskell/app/Main.hs + +cd /opt/hello-haskell +cabal build >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +LOG=$(mktemp) + +timeout --signal=TERM 60s cabal run hello-haskell-exe >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 55 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/haskell-server/runner.yaml b/snippets/validators/languages/haskell-server/runner.yaml new file mode 100644 index 0000000..ee8ff76 --- /dev/null +++ b/snippets/validators/languages/haskell-server/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/haskell-server-validator From 563a5267f082f5e4df27251a30a2be4f5c04ac6e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:28:07 -0700 Subject: [PATCH 12/19] feat(snippets): vue-client-sdk validator + canonical print line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validator builds on the playwright base image: pre-bakes a Vue 3 + Vite project with launchdarkly-vue-client-sdk@2.5.0, vue@3.5.33, vite@8.0.10 installed and warmed. Per-validate stages the snippet's src/main.js and src/App.vue, runs vite build, starts vite preview, and points headless Chromium at it. Snippet's canonical line was "Feature Flag {{ featureKey }} is {{ flagValue }}", which doesn't match the EXAM-HELLO success regex. Updated to "The {{ featureKey }} feature flag evaluates to {{ flagValue }}." — Vue's runtime mustaches survive the snippet template engine via foreign-template pass-through. --- .github/workflows/snippets-validate.yml | 5 +- .../getting-started/app-vue.snippet.md | 7 +- .../languages/vue-client/Dockerfile | 66 +++++++++++++++++++ .../languages/vue-client/harness/check.js | 47 +++++++++++++ .../languages/vue-client/harness/run.sh | 39 +++++++++++ .../languages/vue-client/runner.yaml | 3 + 6 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 snippets/validators/languages/vue-client/Dockerfile create mode 100644 snippets/validators/languages/vue-client/harness/check.js create mode 100755 snippets/validators/languages/vue-client/harness/run.sh create mode 100644 snippets/validators/languages/vue-client/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index cc9e4f2..ee21ae6 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -60,11 +60,12 @@ jobs: - { sdk: haskell-server-sdk, runs-on: ubuntu-latest, key-type: server } # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } + - { sdk: vue-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): # server-runtime: erlang-server-sdk - # browser: react-client-sdk, react-client-sdk-legacy, - # vue-client-sdk (key-type: client) + # browser: react-client-sdk, react-client-sdk-legacy + # (key-type: client) # Linux mobile/native: android-client-sdk, flutter-client-sdk, # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) diff --git a/snippets/sdks/vue-client-sdk/snippets/getting-started/app-vue.snippet.md b/snippets/sdks/vue-client-sdk/snippets/getting-started/app-vue.snippet.md index e8fd152..fdcd702 100644 --- a/snippets/sdks/vue-client-sdk/snippets/getting-started/app-vue.snippet.md +++ b/snippets/sdks/vue-client-sdk/snippets/getting-started/app-vue.snippet.md @@ -11,7 +11,10 @@ inputs: description: Default flag key baked into the rendered source. ld-application: slot: app-vue -# Validator pending — same as main-js. +validation: + runtime: vue-client + entrypoint: src/App.vue + companions: [vue-client-sdk/getting-started/main-js] --- In `src/App.vue`: @@ -25,7 +28,7 @@ const flagValue = useLDFlag('{{ featureKey }}', false) ``` diff --git a/snippets/validators/languages/vue-client/Dockerfile b/snippets/validators/languages/vue-client/Dockerfile new file mode 100644 index 0000000..015b7e5 --- /dev/null +++ b/snippets/validators/languages/vue-client/Dockerfile @@ -0,0 +1,66 @@ +FROM mcr.microsoft.com/playwright:v1.59.1-noble + +# Pre-bake a minimal Vue 3 + Vite project with the LD Vue SDK and a stable +# index.html. Per-validate cycles drop the snippet's main.js + App.vue +# into src/ and re-run vite build; the SDK + Vue + Vite are already cached. +ARG LD_VUE_SDK_VERSION=2.5.0 +ARG VUE_VERSION=3.5.33 +ARG VITE_VERSION=8.0.10 +ARG VITE_PLUGIN_VUE_VERSION=6.0.6 + +RUN mkdir -p /opt/hello-vue/src +WORKDIR /opt/hello-vue + +RUN printf '{\n\ + "name": "hello-vue",\n\ + "private": true,\n\ + "version": "0.0.0",\n\ + "type": "module",\n\ + "scripts": {\n\ + "dev": "vite",\n\ + "build": "vite build",\n\ + "preview": "vite preview --port=4173 --host"\n\ + },\n\ + "dependencies": {\n\ + "launchdarkly-vue-client-sdk": "%s",\n\ + "vue": "%s"\n\ + },\n\ + "devDependencies": {\n\ + "@vitejs/plugin-vue": "%s",\n\ + "vite": "%s"\n\ + }\n\ +}\n' "${LD_VUE_SDK_VERSION}" "${VUE_VERSION}" "${VITE_PLUGIN_VUE_VERSION}" "${VITE_VERSION}" > package.json + +RUN printf "import { defineConfig } from 'vite'\n\ +import vue from '@vitejs/plugin-vue'\n\ +\n\ +export default defineConfig({\n\ + plugins: [vue()],\n\ +})\n" > vite.config.js + +RUN printf '\n\ +\n\ + hello-vue\n\ + \n\ +
\n\ + \n\ + \n\ +\n' > index.html + +# Placeholder src files. Replaced per-validate. +RUN printf "import { createApp } from 'vue'\nimport App from './App.vue'\ncreateApp(App).mount('#app')\n" > src/main.js +RUN printf "\n" > src/App.vue + +RUN npm install --no-audit --no-fund --no-progress + +# Pre-warm the build to seed Vite's transform cache. +RUN npm run build + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/vue-client/harness /harness + +RUN cd /harness && npm install --silent --no-audit --no-fund --no-progress playwright@1.59.1 + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/vue-client/harness/check.js b/snippets/validators/languages/vue-client/harness/check.js new file mode 100644 index 0000000..8d137e5 --- /dev/null +++ b/snippets/validators/languages/vue-client/harness/check.js @@ -0,0 +1,47 @@ +// Loads the locally-served Vue app in headless Chromium, polls the page +// text for the EXAM-HELLO success line, and exits 0 when matched. +const { chromium } = require('/harness/node_modules/playwright'); + +(async () => { + const url = process.env.VUE_PREVIEW_URL || 'http://localhost:4173'; + const successRe = /feature flag evaluates to true/i; + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + page.on('console', msg => console.log('[browser]', msg.text())); + page.on('pageerror', err => console.log('[browser:error]', err.message)); + + // Vite preview may take ~250ms to be ready after spawn. Tolerate ECONNREFUSED. + for (let i = 0; i < 25; i++) { + try { + await page.goto(url); + break; + } catch { + await new Promise(r => setTimeout(r, 200)); + } + } + + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + const text = (await page.textContent('body')) || ''; + if (successRe.test(text)) { + console.log(text.trim()); + console.log('validator: ok'); + await browser.close(); + process.exit(0); + } + await page.waitForTimeout(500); + } + + const finalText = (await page.textContent('body')) || ''; + console.error('validator: did not see expected line: feature flag evaluates to true'); + console.error('--- final body text ---'); + console.error(finalText.trim()); + await browser.close(); + process.exit(1); +})().catch((err) => { + console.error('validator: harness error:', err.message); + process.exit(1); +}); diff --git a/snippets/validators/languages/vue-client/harness/run.sh b/snippets/validators/languages/vue-client/harness/run.sh new file mode 100755 index 0000000..12698ab --- /dev/null +++ b/snippets/validators/languages/vue-client/harness/run.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Builds the staged Vue snippet against the pre-baked Vite project, +# starts a preview server, and runs the Playwright DOM check against it. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_CLIENT_SIDE_ID LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +# Stage the snippet body + its companion (main.js) into the pre-baked +# Vue project. The snippet's `file:` paths are project-relative +# (src/App.vue, src/main.js). +cp "/snippet/src/main.js" /opt/hello-vue/src/main.js +cp "/snippet/$SNIPPET_ENTRYPOINT" "/opt/hello-vue/$SNIPPET_ENTRYPOINT" + +cd /opt/hello-vue +npm run build >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +# Start vite preview in the background and let Playwright probe it. +PREVIEW_LOG=$(mktemp) +npm run preview >"$PREVIEW_LOG" 2>&1 & +PREVIEW_PID=$! + +# Wait briefly for the server to come up. vite preview prints a "Local:" +# line within ~1s on this image. +for _ in $(seq 1 20); do + if grep -q 'Local:' "$PREVIEW_LOG" 2>/dev/null; then + break + fi + sleep 0.2 +done + +cleanup() { + kill -TERM "$PREVIEW_PID" 2>/dev/null || true + wait "$PREVIEW_PID" 2>/dev/null || true +} +trap cleanup EXIT + +VUE_PREVIEW_URL=http://localhost:4173 exec node /harness/check.js diff --git a/snippets/validators/languages/vue-client/runner.yaml b/snippets/validators/languages/vue-client/runner.yaml new file mode 100644 index 0000000..c6f452b --- /dev/null +++ b/snippets/validators/languages/vue-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/vue-client-validator From 08e9544d7fae03cb51ece7489a1f6737979f4638 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:36:54 -0700 Subject: [PATCH 13/19] fix(snippets): align vue-client user key with hello-app convention CI's hello-boolean flag targets contexts whose user key is example-user-key; the vue snippet's main.js used example-user, so the flag returned its default (false) in CI. All other validated client SDKs (node-client, dotnet-client, ios, android, react-native, js-client) already use example-user-key. Cleaning up the rendered comment marker on the snippet at the same time. --- .../vue-client-sdk/snippets/getting-started/main-js.snippet.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md b/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md index 3da329b..1228d44 100644 --- a/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md +++ b/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md @@ -11,7 +11,6 @@ inputs: description: Client-side ID baked into the rendered source. ld-application: slot: main-js -# Validator pending — Vue + Vite + Playwright harness deferred. --- In `src/main.js`: @@ -24,7 +23,7 @@ import { LDPlugin } from 'launchdarkly-vue-client-sdk' const app = createApp(App) app.use(LDPlugin, { clientSideID: '{{ environmentId }}', - context: { kind: 'user', key: 'example-user' }, + context: { kind: 'user', key: 'example-user-key' }, }) app.mount('#app') ``` From 7d489ffac2674d3610a94df4a4b421d61d6118a7 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:57:03 -0700 Subject: [PATCH 14/19] feat(snippets): camelCase filter + react-client-sdk legacy validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine: extend the template language with `{{ name | filter }}`. Today only `camelCase` is supported. For ld-application this renders as `${filter(name)}` (matching gonfalon's existing camelCase use); for validation the filter is applied to the env-var value (`sample-feature` becomes `sampleFeature`). Undeclared names pass through with the filter preserved, so foreign templates aren't disturbed. This unblocks react-client-sdk's snippets, where useFlags() destructures camelCased identifiers from kebab-cased flag keys: the snippet body now uses `{{ featureKey | camelCase }}`. Updated app-tsx and legacy-app-tsx accordingly. Validator: Vite + React + launchdarkly-react-client-sdk@3.9.0 image with the bundle pre-warmed. Per-validate stages App.tsx + the entrypoint (index.tsx for legacy, main.tsx for createApp), rewrites index.html to point at it, builds, serves vite preview, and probes the DOM with Playwright. Wired the legacy variant: legacy-app-tsx now declares validation.runtime + companions, and gonfalon's legacy.tsx drops its hand-authored "camelCase filter pending" Snippet in favor of the marker-driven render (byte-equivalent JSX, just template-literal-formatted differently). The createApp variant stays unwired — it needs gonfalon's createApp.tsx to drop its assetSource/replaceAll pattern, plus the snippet's needs an explicit context= for the hello-app's user key. --- .github/workflows/snippets-validate.yml | 7 +- snippets/internal/render/render.go | 85 +++++++++++++++-- snippets/internal/render/template.go | 25 +++-- .../getting-started/app-tsx.snippet.md | 6 +- .../getting-started/legacy-app-tsx.snippet.md | 10 +- .../languages/react-client/Dockerfile | 91 +++++++++++++++++++ .../languages/react-client/harness/check.js | 46 ++++++++++ .../languages/react-client/harness/run.sh | 64 +++++++++++++ .../languages/react-client/runner.yaml | 3 + 9 files changed, 315 insertions(+), 22 deletions(-) create mode 100644 snippets/validators/languages/react-client/Dockerfile create mode 100644 snippets/validators/languages/react-client/harness/check.js create mode 100755 snippets/validators/languages/react-client/harness/run.sh create mode 100644 snippets/validators/languages/react-client/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index ee21ae6..d054bb3 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -61,11 +61,14 @@ jobs: # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: vue-client-sdk, runs-on: ubuntu-latest, key-type: client } + - { sdk: react-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): # server-runtime: erlang-server-sdk - # browser: react-client-sdk, react-client-sdk-legacy - # (key-type: client) + # browser: react-client-sdk createApp variant — pending the gonfalon + # createApp.tsx refactor (replaceAll → inline template literal). + # The React SDK's createApp main-tsx snippet also needs explicit + # context= on to target the hello-app's user key. # Linux mobile/native: android-client-sdk, flutter-client-sdk, # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) diff --git a/snippets/internal/render/render.go b/snippets/internal/render/render.go index 5c41b4c..64bb804 100644 --- a/snippets/internal/render/render.go +++ b/snippets/internal/render/render.go @@ -19,11 +19,12 @@ func RenderRuntime(nodes []Node, inputs map[string]string) (string, error) { v, ok := inputs[x.Name] if !ok { // Unknown name — emit verbatim so foreign templates pass through. - sb.WriteString("{{ ") - sb.WriteString(x.Name) - sb.WriteString(" }}") + sb.WriteString(literalVar(x)) continue } + if x.Filter != "" { + v = applyFilter(x.Filter, v) + } sb.WriteString(v) case *Cond: v, ok := inputs[x.Var] @@ -80,11 +81,18 @@ func RenderForLDApplicationTemplate(nodes []Node, declaredInputs map[string]stru if _, ok := declaredInputs[x.Name]; !ok { // Foreign template — emit the original `{{ name }}` literally, // escaped for the surrounding template literal. - sb.WriteString(escapeTL("{{ " + x.Name + " }}")) + sb.WriteString(escapeTL(literalVar(x))) continue } sb.WriteString("${") - sb.WriteString(x.Name) + if x.Filter != "" { + sb.WriteString(x.Filter) + sb.WriteString("(") + sb.WriteString(x.Name) + sb.WriteString(")") + } else { + sb.WriteString(x.Name) + } sb.WriteString("}") case *Cond: sb.WriteString("${") @@ -117,9 +125,7 @@ func RenderForJSXText(nodes []Node, declaredInputs map[string]struct{}) (string, return "", fmt.Errorf("RenderForJSXText: template has declared interpolation; use RenderForLDApplicationTemplate") } // Foreign template — emit literal. - sb.WriteString("{{ ") - sb.WriteString(x.Name) - sb.WriteString(" }}") + sb.WriteString(literalVar(x)) case *Cond: return "", fmt.Errorf("RenderForJSXText: template has conditional; use RenderForLDApplicationTemplate") } @@ -142,3 +148,66 @@ func escapeTL(s string) string { s = strings.ReplaceAll(s, "${", "\\${") return s } + +// literalVar formats a Var node back to its source `{{ name }}` / +// `{{ name | filter }}` form. Used when emitting an undeclared name +// verbatim so foreign-template syntax round-trips intact. +func literalVar(v *Var) string { + if v.Filter == "" { + return "{{ " + v.Name + " }}" + } + return "{{ " + v.Name + " | " + v.Filter + " }}" +} + +// applyFilter applies a filter to a runtime value. Today only `camelCase` +// is supported — used by react-client-sdk's snippets where useFlags() +// destructures camelCased identifiers from a kebab-cased flag key. +func applyFilter(name, value string) string { + switch name { + case "camelCase": + return camelCase(value) + default: + return value + } +} + +// camelCase mirrors @gonfalon/strings' camelCase. Converts kebab-case, +// snake_case, and space-separated words to camelCase. Leading non-alpha +// runs are stripped. The first segment stays lowercase; subsequent +// segments get an uppercase initial. +// +// Examples: +// sample-feature -> sampleFeature +// my_flag_key -> myFlagKey +// already-camelOK -> alreadyCamelOk (lowercases later segments first) +func camelCase(s string) string { + var segs []string + var cur strings.Builder + flush := func() { + if cur.Len() > 0 { + segs = append(segs, strings.ToLower(cur.String())) + cur.Reset() + } + } + for _, r := range s { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { + cur.WriteRune(r) + continue + } + flush() + } + flush() + if len(segs) == 0 { + return "" + } + var out strings.Builder + out.WriteString(segs[0]) + for _, seg := range segs[1:] { + if seg == "" { + continue + } + out.WriteString(strings.ToUpper(seg[:1])) + out.WriteString(seg[1:]) + } + return out.String() +} diff --git a/snippets/internal/render/template.go b/snippets/internal/render/template.go index f32e43d..d5d9471 100644 --- a/snippets/internal/render/template.go +++ b/snippets/internal/render/template.go @@ -9,16 +9,21 @@ import ( // The snippet templating language is intentionally tiny: // // {{ varName }} substitute the value of an input +// {{ varName | filter }} substitute the value with a filter applied // {{ if varName }}...{{ end }} emit "..." only if the input is truthy (non-empty) // // Conditionals do not nest in the first-pass slice. The inner "..." may still -// contain {{ varName }} substitutions. Future phases can extend this (filters, -// region toggles, version toggles) without breaking existing snippets. +// contain {{ varName }} substitutions. Filters supported today: `camelCase`. +// React's snippets need the camelCase filter because `useFlags()` destructures +// camelCased identifiers; ld-application maps these to `${camelCase(name)}`. type Node interface{ isNode() } type Literal struct{ Text string } -type Var struct{ Name string } +type Var struct { + Name string + Filter string // empty if no filter; e.g. "camelCase" +} type Cond struct { Var string Body []Node @@ -29,7 +34,7 @@ func (*Var) isNode() {} func (*Cond) isNode() {} var nameRe = `[a-zA-Z][a-zA-Z0-9_]*` -var tokenRe = regexp.MustCompile(`\{\{\s*(if\s+(` + nameRe + `)\s*|end\s*|(` + nameRe + `)\s*)\}\}`) +var tokenRe = regexp.MustCompile(`\{\{\s*(if\s+(` + nameRe + `)\s*|end\s*|(` + nameRe + `)(?:\s*\|\s*(` + nameRe + `))?\s*)\}\}`) // Parse parses the mini-templating syntax into a flat node list. // Conditionals are flattened: a Cond node contains its inner body. @@ -71,9 +76,17 @@ func Parse(src string) ([]Node, error) { closed := stack[len(stack)-1] stack = stack[:len(stack)-1] append_(closed) - case m[6] >= 0: // "NAME" + case m[6] >= 0: // "NAME" optionally followed by "| FILTER" name := src[m[6]:m[7]] - append_(&Var{Name: name}) + v := &Var{Name: name} + if m[8] >= 0 { + filter := src[m[8]:m[9]] + if filter != "camelCase" { + return nil, fmt.Errorf("template: unknown filter %q at offset %d (only `camelCase` is supported)", filter, start) + } + v.Filter = filter + } + append_(v) default: return nil, fmt.Errorf("template: unrecognized directive %q at offset %d", token, start) } diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md index 7783503..cc95dbd 100644 --- a/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -19,11 +19,11 @@ Use the `useFlags` hook to evaluate flags. For example, in `App.tsx`: import { useFlags } from 'launchdarkly-react-client-sdk'; function App() { - const { {{ featureKey }} } = useFlags(); + const { {{ featureKey | camelCase }} } = useFlags(); return ( -
- The {{ featureKey }} feature flag evaluates to { {{ featureKey }} ? 'true' : 'false'} +
+ The {{ featureKey | camelCase }} feature flag evaluates to { {{ featureKey | camelCase }} ? 'true' : 'false'}
); } diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md index 0e80977..d0ea28c 100644 --- a/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md @@ -11,6 +11,10 @@ inputs: description: Default flag key (camelCased) baked into the rendered source. Note that gonfalon camel-cases the supplied flag key before substituting; for validation we use the env-var value as-is. ld-application: slot: legacy-app-tsx +validation: + runtime: react-client + entrypoint: src/App.tsx + companions: [react-client-sdk/getting-started/legacy-index-tsx] --- In `App.tsx`: @@ -20,12 +24,12 @@ import './App.css'; import { useFlags } from 'launchdarkly-react-client-sdk'; function App() { - const { {{ featureKey }} } = useFlags(); + const { {{ featureKey | camelCase }} } = useFlags(); return (
-
-

The {{ featureKey }} feature flag evaluates to { {{ featureKey }} ? 'True' : 'False'}

+
+

The {{ featureKey | camelCase }} feature flag evaluates to { {{ featureKey | camelCase }} ? 'True' : 'False'}

); diff --git a/snippets/validators/languages/react-client/Dockerfile b/snippets/validators/languages/react-client/Dockerfile new file mode 100644 index 0000000..698d2a9 --- /dev/null +++ b/snippets/validators/languages/react-client/Dockerfile @@ -0,0 +1,91 @@ +FROM mcr.microsoft.com/playwright:v1.59.1-noble + +# Pre-bake a minimal Vite + React + TypeScript project with the LD React +# SDK installed. Per-validate cycles drop the snippet's entrypoint +# (src/index.tsx for legacy, src/main.tsx for createApp) plus its App.tsx +# companion into src/ and re-run vite build. +ARG LD_REACT_SDK_VERSION=3.9.0 +ARG REACT_VERSION=19.2.5 +ARG VITE_VERSION=8.0.10 +ARG VITE_PLUGIN_REACT_VERSION=6.0.1 + +RUN mkdir -p /opt/hello-react/src +WORKDIR /opt/hello-react + +RUN printf '{\n\ + "name": "hello-react",\n\ + "private": true,\n\ + "version": "0.0.0",\n\ + "type": "module",\n\ + "scripts": {\n\ + "build": "vite build",\n\ + "preview": "vite preview --port=4173 --host"\n\ + },\n\ + "dependencies": {\n\ + "launchdarkly-react-client-sdk": "%s",\n\ + "react": "%s",\n\ + "react-dom": "%s"\n\ + },\n\ + "devDependencies": {\n\ + "@types/react": "19.2.5",\n\ + "@types/react-dom": "19.2.3",\n\ + "@vitejs/plugin-react": "%s",\n\ + "typescript": "5.6.3",\n\ + "vite": "%s"\n\ + }\n\ +}\n' \ + "${LD_REACT_SDK_VERSION}" "${REACT_VERSION}" "${REACT_VERSION}" \ + "${VITE_PLUGIN_REACT_VERSION}" "${VITE_VERSION}" > package.json + +RUN printf "import { defineConfig } from 'vite'\n\ +import react from '@vitejs/plugin-react'\n\ +\n\ +export default defineConfig({\n\ + plugins: [react()],\n\ +})\n" > vite.config.js + +RUN printf '{\n\ + "compilerOptions": {\n\ + "target": "ES2020",\n\ + "lib": ["ES2020", "DOM", "DOM.Iterable"],\n\ + "module": "ESNext",\n\ + "moduleResolution": "bundler",\n\ + "jsx": "react-jsx",\n\ + "strict": false,\n\ + "isolatedModules": true,\n\ + "noEmit": true,\n\ + "skipLibCheck": true\n\ + },\n\ + "include": ["src"]\n\ +}\n' > tsconfig.json + +# Empty stylesheet — both legacy and createApp variants reference one. +RUN touch src/index.css src/App.css + +# Placeholder entrypoint + App. Replaced per-validate. The harness rewrites +# index.html to point at whichever entrypoint the snippet declared. +RUN printf "import { createRoot } from 'react-dom/client';\nimport App from './App';\ncreateRoot(document.getElementById('root')).render();\n" > src/main.tsx +RUN printf "export default function App() { return
placeholder
; }\n" > src/App.tsx + +RUN printf '\n\ +\n\ + hello-react\n\ + \n\ +
\n\ + \n\ + \n\ +\n' > index.html + +RUN npm install --no-audit --no-fund --no-progress + +# Pre-warm the build to seed Vite's transform cache + esbuild prebundle. +RUN npm run build + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/react-client/harness /harness + +RUN cd /harness && npm install --silent --no-audit --no-fund --no-progress playwright@1.59.1 + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/react-client/harness/check.js b/snippets/validators/languages/react-client/harness/check.js new file mode 100644 index 0000000..e1acf34 --- /dev/null +++ b/snippets/validators/languages/react-client/harness/check.js @@ -0,0 +1,46 @@ +// Loads the locally-served React app in headless Chromium, polls the +// page text for the EXAM-HELLO success line, and exits 0 when matched. +const { chromium } = require('/harness/node_modules/playwright'); + +(async () => { + const url = process.env.REACT_PREVIEW_URL || 'http://localhost:4173'; + const successRe = /feature flag evaluates to true/i; + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + page.on('console', msg => console.log('[browser]', msg.text())); + page.on('pageerror', err => console.log('[browser:error]', err.message)); + + for (let i = 0; i < 25; i++) { + try { + await page.goto(url); + break; + } catch { + await new Promise(r => setTimeout(r, 200)); + } + } + + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + const text = (await page.textContent('body')) || ''; + if (successRe.test(text)) { + console.log(text.trim()); + console.log('validator: ok'); + await browser.close(); + process.exit(0); + } + await page.waitForTimeout(500); + } + + const finalText = (await page.textContent('body')) || ''; + console.error('validator: did not see expected line: feature flag evaluates to true'); + console.error('--- final body text ---'); + console.error(finalText.trim()); + await browser.close(); + process.exit(1); +})().catch((err) => { + console.error('validator: harness error:', err.message); + process.exit(1); +}); diff --git a/snippets/validators/languages/react-client/harness/run.sh b/snippets/validators/languages/react-client/harness/run.sh new file mode 100755 index 0000000..99b6544 --- /dev/null +++ b/snippets/validators/languages/react-client/harness/run.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# Builds the staged React snippet against the pre-baked Vite project, +# starts a preview server, and runs the Playwright DOM check against it. +# +# Two snippet variants flow through this same harness: the legacy CRA +# pattern uses src/index.tsx, while the createApp/Vite pattern uses +# src/main.tsx. We rewrite index.html to point at whichever entrypoint +# the snippet declared. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_CLIENT_SIDE_ID LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +# Stage the snippet body (App.tsx) plus its companion (index.tsx or +# main.tsx) into the pre-baked React project. +cp "/snippet/$SNIPPET_ENTRYPOINT" "/opt/hello-react/$SNIPPET_ENTRYPOINT" +for f in /snippet/src/*.tsx; do + [ -f "$f" ] || continue + bn=$(basename "$f") + cp "$f" "/opt/hello-react/src/$bn" +done + +# Point index.html at whichever entrypoint the snippet uses (index.tsx +# for legacy, main.tsx for createApp). +ENTRY_BASENAME=$(basename "$SNIPPET_ENTRYPOINT") +ENTRY_FILE="src/$(basename "$SNIPPET_ENTRYPOINT" .tsx).tsx" +# Companion may also be the entrypoint script. Pick whichever isn't App.tsx. +SCRIPT_SRC="" +for f in /opt/hello-react/src/*.tsx; do + bn=$(basename "$f") + case "$bn" in + App.tsx) ;; + *) SCRIPT_SRC="/src/$bn"; break ;; + esac +done +if [ -z "$SCRIPT_SRC" ]; then + echo "harness: could not find non-App entrypoint in /opt/hello-react/src" >&2 + exit 1 +fi + +cd /opt/hello-react +sed -i "s|/src/main.tsx|$SCRIPT_SRC|" index.html + +npm run build >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +PREVIEW_LOG=$(mktemp) +npm run preview >"$PREVIEW_LOG" 2>&1 & +PREVIEW_PID=$! + +for _ in $(seq 1 20); do + if grep -q 'Local:' "$PREVIEW_LOG" 2>/dev/null; then + break + fi + sleep 0.2 +done + +cleanup() { + kill -TERM "$PREVIEW_PID" 2>/dev/null || true + wait "$PREVIEW_PID" 2>/dev/null || true +} +trap cleanup EXIT + +REACT_PREVIEW_URL=http://localhost:4173 exec node /harness/check.js diff --git a/snippets/validators/languages/react-client/runner.yaml b/snippets/validators/languages/react-client/runner.yaml new file mode 100644 index 0000000..a910da4 --- /dev/null +++ b/snippets/validators/languages/react-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/react-client-validator From 3e2b79b92aa03fd1d83890e434b9452bb1f82a0f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:14:37 -0700 Subject: [PATCH 15/19] feat(snippets): wire react-client-sdk createApp variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema: extend ld-application with a `get-started-files: [list]` field alongside the existing `get-started-file: string`, so a single SDK can target multiple consumer files. react-client-sdk now renders into both gonfalon variants (legacy.tsx + createApp.tsx). Gonfalon: refactor createApp.tsx off its assetSource `?raw + replaceAll` pattern. The snippet system can't intercept that — replacing it with inline template literals so the markers do their normal job. The assetSource/main.tsx.txt and assetSource/App.tsx.txt files are no longer imported but left in place to keep this PR focused on the createApp.tsx behavior. Snippet: createApp's main-tsx was missing explicit context= on , so the SDK evaluated against an anonymous user and hello-boolean returned its default. Added the standard example-user-key context, matching the legacy variant + every other client SDK. Validator: same react-client runtime as legacy. With both variants now declared validation.runtime, the CI cell exercises both in sequence within the react-client-sdk job. --- .github/workflows/snippets-validate.yml | 4 --- .../adapters/ldapplication/descriptor.go | 3 +- .../adapters/ldapplication/ldapplication.go | 34 ++++++++++--------- snippets/sdks/react-client-sdk/sdk.yaml | 15 ++++---- .../getting-started/app-tsx.snippet.md | 4 +++ .../getting-started/main-tsx.snippet.md | 6 ++-- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index d054bb3..649f786 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -65,10 +65,6 @@ jobs: # # Validators not yet wired (snippets ported, harness pending): # server-runtime: erlang-server-sdk - # browser: react-client-sdk createApp variant — pending the gonfalon - # createApp.tsx refactor (replaceAll → inline template literal). - # The React SDK's createApp main-tsx snippet also needs explicit - # context= on to target the hello-app's user key. # Linux mobile/native: android-client-sdk, flutter-client-sdk, # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) diff --git a/snippets/internal/adapters/ldapplication/descriptor.go b/snippets/internal/adapters/ldapplication/descriptor.go index 3a247cb..541d0cc 100644 --- a/snippets/internal/adapters/ldapplication/descriptor.go +++ b/snippets/internal/adapters/ldapplication/descriptor.go @@ -22,7 +22,8 @@ type descriptor struct { Regions []string `yaml:"regions"` HelloWorldRepo string `yaml:"hello-world-repo"` LDApplication struct { - GetStartedFile string `yaml:"get-started-file"` + GetStartedFile string `yaml:"get-started-file"` + GetStartedFiles []string `yaml:"get-started-files"` } `yaml:"ld-application"` Docs struct { ReferencePage string `yaml:"reference-page"` diff --git a/snippets/internal/adapters/ldapplication/ldapplication.go b/snippets/internal/adapters/ldapplication/ldapplication.go index 2fc75e5..fc84675 100644 --- a/snippets/internal/adapters/ldapplication/ldapplication.go +++ b/snippets/internal/adapters/ldapplication/ldapplication.go @@ -92,24 +92,26 @@ func discoverTargetFiles(sdksDir, appDir string) ([]string, error) { } return nil, err } - rel := desc.LDApplication.GetStartedFile - if rel == "" { - continue - } - if filepath.IsAbs(rel) { - return nil, fmt.Errorf("descriptor %s: get-started-file %q must be relative", descPath, rel) - } - full := filepath.Join(absAppDir, rel) - // Reject any path that escapes appDir. filepath.Rel followed by a - // `..` prefix check is the canonical way to do this. - relCheck, err := filepath.Rel(absAppDir, full) - if err != nil || relCheck == ".." || strings.HasPrefix(relCheck, ".."+string(filepath.Separator)) { - return nil, fmt.Errorf("descriptor %s: get-started-file %q escapes appDir", descPath, rel) + rels := desc.LDApplication.GetStartedFiles + if rel := desc.LDApplication.GetStartedFile; rel != "" { + rels = append([]string{rel}, rels...) } - if _, err := os.Stat(full); err != nil { - return nil, fmt.Errorf("descriptor %s: target not found: %w", descPath, err) + for _, rel := range rels { + if filepath.IsAbs(rel) { + return nil, fmt.Errorf("descriptor %s: get-started-file %q must be relative", descPath, rel) + } + full := filepath.Join(absAppDir, rel) + // Reject any path that escapes appDir. filepath.Rel followed by a + // `..` prefix check is the canonical way to do this. + relCheck, err := filepath.Rel(absAppDir, full) + if err != nil || relCheck == ".." || strings.HasPrefix(relCheck, ".."+string(filepath.Separator)) { + return nil, fmt.Errorf("descriptor %s: get-started-file %q escapes appDir", descPath, rel) + } + if _, err := os.Stat(full); err != nil { + return nil, fmt.Errorf("descriptor %s: target not found: %w", descPath, err) + } + out = append(out, full) } - out = append(out, full) } return out, nil } diff --git a/snippets/sdks/react-client-sdk/sdk.yaml b/snippets/sdks/react-client-sdk/sdk.yaml index c35c006..449df51 100644 --- a/snippets/sdks/react-client-sdk/sdk.yaml +++ b/snippets/sdks/react-client-sdk/sdk.yaml @@ -10,13 +10,14 @@ languages: package-managers: [npm, yarn] regions: [commercial, federal, eu, relay-proxy] ld-application: - # Legacy variant (create-react-app) uses standard JSX template-literal - # interpolation and is rendered into gonfalon via render markers. The - # createApp (Vite) variant uses `?raw` asset-file imports + replaceAll; - # the snippet files are present in this dir but rendering into - # createApp.tsx is deferred until that file is migrated off the - # assetSource pattern. - get-started-file: static/ld/components/getStarted/sdk/react/legacy.tsx + # Two get-started variants live side-by-side in gonfalon: the legacy + # create-react-app flow (legacy.tsx) and the createApp/Vite flow + # (createApp.tsx). Both use standard JSX template-literal interpolation + # with the `${camelCase(featureKey)}` pattern, so both render via + # render markers. + get-started-files: + - static/ld/components/getStarted/sdk/react/legacy.tsx + - static/ld/components/getStarted/sdk/react/createApp.tsx docs: reference-page: /sdk/client-side/react/react-web hello-world-repo: launchdarkly-labs/react-ts diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md index cc95dbd..d376498 100644 --- a/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -11,6 +11,10 @@ inputs: description: Default flag key (camelCased) baked into the rendered source. ld-application: slot: app-tsx +validation: + runtime: react-client + entrypoint: src/App.tsx + companions: [react-client-sdk/getting-started/main-tsx] --- Use the `useFlags` hook to evaluate flags. For example, in `App.tsx`: diff --git a/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md b/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md index 327c4a0..29b1e1d 100644 --- a/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md @@ -11,7 +11,6 @@ inputs: description: Client-side ID baked into the rendered source. ld-application: slot: main-tsx -# Validator pending — Vite build + Playwright headless harness deferred. --- In `main.tsx`, wrap your application with `LDProvider`: @@ -24,7 +23,10 @@ import App from './App'; createRoot(document.getElementById('root')!).render( - + , From 966712a04b521fe5e020fb54674f5d42af5c822f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:32:20 -0700 Subject: [PATCH 16/19] feat(snippets): flutter-client-sdk validator (web target) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validator runs Flutter's web build target — the only Linux-native path for an SDK whose other targets need iOS/Android tooling. The Dockerfile extends the playwright base with the Flutter SDK at /opt/flutter, pre-creates a hello_flutter project, pins launchdarkly_flutter_client_sdk@4.16.0 + provider@6.1.2, and pre-warms `flutter build web` so per-validate cycles only recompile main.dart. Per-validate: stage lib/main.dart, run `flutter build web` with `--dart-define LAUNCHDARKLY_CLIENT_SIDE_ID=...` so the snippet's CredentialSource.fromEnvironment() picks the credential up at compile time, static-serve build/web with python's http.server, probe the DOM with Playwright. The harness clicks Flutter's to force the semantics tree to populate text into the DOM where Playwright can read it. Snippet itself is a clean port — gonfalon's main.dart already prints the canonical EXAM-HELLO line via Dart string interpolation. --- .github/workflows/snippets-validate.yml | 3 +- .../getting-started/main-dart.snippet.md | 10 ++- .../languages/flutter-client/Dockerfile | 53 +++++++++++++++ .../languages/flutter-client/harness/check.js | 65 +++++++++++++++++++ .../languages/flutter-client/harness/run.sh | 43 ++++++++++++ .../languages/flutter-client/runner.yaml | 3 + 6 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 snippets/validators/languages/flutter-client/Dockerfile create mode 100644 snippets/validators/languages/flutter-client/harness/check.js create mode 100755 snippets/validators/languages/flutter-client/harness/run.sh create mode 100644 snippets/validators/languages/flutter-client/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 649f786..f89195e 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -62,10 +62,11 @@ jobs: - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: vue-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: react-client-sdk, runs-on: ubuntu-latest, key-type: client } + - { sdk: flutter-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): # server-runtime: erlang-server-sdk - # Linux mobile/native: android-client-sdk, flutter-client-sdk, + # Linux mobile/native: android-client-sdk, # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) # Skipped: roku-client-sdk (validation: none, manual procedure). diff --git a/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md b/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md index 75d733d..bb3758f 100644 --- a/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md @@ -11,8 +11,14 @@ inputs: description: Default flag key baked into the rendered source. ld-application: slot: main-dart -# Validator pending — flutter test/run on a Linux runner with -# flutter-action; deferred. +validation: + runtime: flutter-client + entrypoint: lib/main.dart + # The credential isn't substituted into the source — the snippet uses + # CredentialSource.fromEnvironment() which reads + # LAUNCHDARKLY_CLIENT_SIDE_ID baked in via flutter's --dart-define + # flag at build time. The validator passes that env var through; no + # `inputs:` declaration needed since nothing is interpolated here. --- Open the file `lib/main.dart` and replace with the following code: diff --git a/snippets/validators/languages/flutter-client/Dockerfile b/snippets/validators/languages/flutter-client/Dockerfile new file mode 100644 index 0000000..ecf95bb --- /dev/null +++ b/snippets/validators/languages/flutter-client/Dockerfile @@ -0,0 +1,53 @@ +FROM mcr.microsoft.com/playwright:v1.59.1-noble + +# Flutter SDK + transitive build dependencies. The snippet's main.dart +# uses `provider` and Material widgets, which compile cleanly to web +# (no native plugins). Web target keeps the validator container in the +# Linux/x86 lane — no emulators or simulators required. +ARG FLUTTER_VERSION=3.27.4 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl xz-utils git unzip ca-certificates \ + # Flutter doctor dependencies for web target: + libgtk-3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Flutter wants a non-root user by default; per their warning we set +# the runner ENV. We're inside an isolated build container so root is fine. +ENV PUB_CACHE=/opt/pub-cache +ENV FLUTTER_HOME=/opt/flutter +ENV PATH="${FLUTTER_HOME}/bin:${PATH}" + +RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" \ + | tar -xJ -C /opt \ + && git config --global --add safe.directory /opt/flutter \ + && flutter config --no-analytics --enable-web + +# Pre-bake a hello-flutter web project mirroring what `flutter create` +# produces, with the SDK + provider deps already pinned. Per-validate +# cycles overwrite lib/main.dart and re-run `flutter build web` against +# the warmed pub cache + dart kernel cache. +ARG LD_FLUTTER_SDK_VERSION=4.16.0 +ARG PROVIDER_VERSION=6.1.2 + +RUN flutter create --platforms=web --org com.launchdarkly /opt/hello_flutter +WORKDIR /opt/hello_flutter + +# Replace the default deps with our pinned set so the lockfile is +# stable and the warmed kernel cache below applies to every per-validate +# build. +RUN sed -i "/^dependencies:/,/^[a-z]/{/^ cupertino_icons/d}" pubspec.yaml \ + && sed -i "/^dependencies:/a\\ launchdarkly_flutter_client_sdk: ${LD_FLUTTER_SDK_VERSION}\\n provider: ${PROVIDER_VERSION}" pubspec.yaml \ + && flutter pub get + +# Pre-warm the web build so first per-validate is incremental. +RUN flutter build web --release --no-pub + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/flutter-client/harness /harness + +RUN cd /harness && npm install --silent --no-audit --no-fund --no-progress playwright@1.59.1 + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/flutter-client/harness/check.js b/snippets/validators/languages/flutter-client/harness/check.js new file mode 100644 index 0000000..36f0fb5 --- /dev/null +++ b/snippets/validators/languages/flutter-client/harness/check.js @@ -0,0 +1,65 @@ +// Loads the locally-served Flutter web bundle in headless Chromium and +// polls the rendered Flutter canvas for the EXAM-HELLO success line. +// +// Flutter web renders text into elements (semantics +// tree) once the page has bootstrapped. Some Flutter releases gate the +// semantics tree behind explicit activation; for those we fall back to +// the raw DOM, which still contains the rendered text in +// HTML mode. We probe both. +const { chromium } = require('/harness/node_modules/playwright'); + +(async () => { + const url = process.env.FLUTTER_PREVIEW_URL || 'http://localhost:4173'; + const successRe = /feature flag evaluates to true/i; + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + page.on('console', msg => console.log('[browser]', msg.text())); + page.on('pageerror', err => console.log('[browser:error]', err.message)); + + for (let i = 0; i < 25; i++) { + try { + await page.goto(url); + break; + } catch { + await new Promise(r => setTimeout(r, 200)); + } + } + + // Force semantic tree on so the Text widget's content lands in the DOM. + await page.evaluate(() => { + if (window.flutterSemanticsTree) return; + const enable = () => { + try { + const placeholder = document.querySelector('flt-semantics-placeholder'); + if (placeholder) placeholder.click(); + } catch {} + }; + enable(); + setTimeout(enable, 1000); + }); + + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + const text = (await page.textContent('body')) || ''; + if (successRe.test(text)) { + console.log(text.replace(/\s+/g, ' ').trim()); + console.log('validator: ok'); + await browser.close(); + process.exit(0); + } + await page.waitForTimeout(500); + } + + const finalText = (await page.textContent('body')) || ''; + console.error('validator: did not see expected line: feature flag evaluates to true'); + console.error('--- final body text ---'); + console.error(finalText.replace(/\s+/g, ' ').trim()); + await browser.close(); + process.exit(1); +})().catch((err) => { + console.error('validator: harness error:', err.message); + process.exit(1); +}); diff --git a/snippets/validators/languages/flutter-client/harness/run.sh b/snippets/validators/languages/flutter-client/harness/run.sh new file mode 100755 index 0000000..d4b2835 --- /dev/null +++ b/snippets/validators/languages/flutter-client/harness/run.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Builds the staged Flutter snippet against the pre-baked web project and +# runs the Playwright DOM check against a static-served bundle. +# +# The snippet's main.dart pulls credentials via +# `CredentialSource.fromEnvironment()`, which reads the +# LAUNCHDARKLY_CLIENT_SIDE_ID / LAUNCHDARKLY_MOBILE_KEY values that were +# baked into the build via --dart-define. Mobile keys aren't valid for +# the web target, so we only forward CLIENT_SIDE_ID here. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_CLIENT_SIDE_ID LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +cp "/snippet/$SNIPPET_ENTRYPOINT" /opt/hello_flutter/lib/main.dart + +cd /opt/hello_flutter + +flutter build web --release --no-pub \ + --dart-define LAUNCHDARKLY_CLIENT_SIDE_ID="${LAUNCHDARKLY_CLIENT_SIDE_ID}" \ + >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +# Static-serve the build output. python3 is in the playwright base image. +PREVIEW_LOG=$(mktemp) +( cd build/web && python3 -m http.server 4173 ) >"$PREVIEW_LOG" 2>&1 & +PREVIEW_PID=$! + +# python's http.server writes "Serving HTTP on …" once it binds. +for _ in $(seq 1 20); do + if grep -q 'Serving' "$PREVIEW_LOG" 2>/dev/null; then + break + fi + sleep 0.2 +done + +cleanup() { + kill -TERM "$PREVIEW_PID" 2>/dev/null || true + wait "$PREVIEW_PID" 2>/dev/null || true +} +trap cleanup EXIT + +FLUTTER_PREVIEW_URL=http://localhost:4173 exec node /harness/check.js diff --git a/snippets/validators/languages/flutter-client/runner.yaml b/snippets/validators/languages/flutter-client/runner.yaml new file mode 100644 index 0000000..d54b680 --- /dev/null +++ b/snippets/validators/languages/flutter-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/flutter-client-validator From a22c670a82694ac2b5be55530051e7be93c0b25f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:41:11 -0700 Subject: [PATCH 17/19] feat(snippets): erlang-server-sdk validator The Get Started flow is interactive (rebar3 shell + manual gen_server:call), so the snippet itself never prints the canonical EXAM-HELLO line. Synthesize the equivalent at validate time: pre-bake a hello_erlang OTP application with rebar.config + app.src + supervisor pinned to launchdarkly_server_sdk@3.9.0, drop in the snippet's hello_erlang_server.erl per-validate, run `rebar3 compile`, and drive `erl -noshell -eval` to ensure_all_started + sleep + hello_erlang_server:get/3 + io:format the canonical line + init:stop. `rebar3 eval` doesn't exist as a built-in task on the bundled image, so we drive `erl` directly with -pa pointing at the compiled beam dirs under _build/default/lib/*/ebin. --- .github/workflows/snippets-validate.yml | 2 +- .../getting-started/server-erl.snippet.md | 14 +-- .../languages/erlang-server/Dockerfile | 93 +++++++++++++++++++ .../languages/erlang-server/harness/run.sh | 48 ++++++++++ .../languages/erlang-server/runner.yaml | 3 + 5 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 snippets/validators/languages/erlang-server/Dockerfile create mode 100755 snippets/validators/languages/erlang-server/harness/run.sh create mode 100644 snippets/validators/languages/erlang-server/runner.yaml diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index f89195e..3d8d0d2 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -58,6 +58,7 @@ jobs: - { sdk: cpp-client-sdk, runs-on: ubuntu-latest, key-type: mobile } - { sdk: lua-server-sdk, runs-on: ubuntu-latest, key-type: server } - { sdk: haskell-server-sdk, runs-on: ubuntu-latest, key-type: server } + - { sdk: erlang-server-sdk, runs-on: ubuntu-latest, key-type: server } # Browser SDKs (headless Playwright): - { sdk: js-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: vue-client-sdk, runs-on: ubuntu-latest, key-type: client } @@ -65,7 +66,6 @@ jobs: - { sdk: flutter-client-sdk, runs-on: ubuntu-latest, key-type: client } # # Validators not yet wired (snippets ported, harness pending): - # server-runtime: erlang-server-sdk # Linux mobile/native: android-client-sdk, # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) diff --git a/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md b/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md index ab5f8fc..8260420 100644 --- a/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md @@ -8,14 +8,16 @@ description: gen_server module that wraps the LaunchDarkly Erlang client. inputs: apiKey: type: sdk-key - description: SDK key baked into the rendered source. Validation is not yet wired — see comment. + description: SDK key baked into the rendered source. ld-application: slot: server-erl -# Validator pending. The Erlang Get Started flow is fundamentally -# interactive (rebar3 shell + manual gen_server:call). To validate end- -# to-end we'd need to write a wrapper application script that calls -# hello_erlang_server:get/3 and prints an EXAM-HELLO conformant line. -# That's a snippet rewrite and is left as fix-on-red. +validation: + runtime: erlang-server + entrypoint: src/hello_erlang_server.erl + # The user-facing flow is interactive: `rebar3 shell` + manual + # gen_server:call. The validator synthesizes the equivalent in + # `rebar3 eval` so the gen_server is exercised end-to-end without + # requiring a wrapper module in the snippet itself. --- First create a new file named `src/hello_erlang_server.erl`. Then, in diff --git a/snippets/validators/languages/erlang-server/Dockerfile b/snippets/validators/languages/erlang-server/Dockerfile new file mode 100644 index 0000000..ddc1d44 --- /dev/null +++ b/snippets/validators/languages/erlang-server/Dockerfile @@ -0,0 +1,93 @@ +FROM erlang:26 + +# rebar3 ships with the official erlang image. +# +# Pre-bake a hello_erlang OTP application matching the structure +# gonfalon's Get Started flow walks the user through: +# - rebar.config pulls launchdarkly_server_sdk from Hex (alias ldclient). +# - hello_erlang.app.src lists ldclient as an application. +# - hello_erlang_sup.erl supervises hello_erlang_server. +# +# Per-validate cycles only swap hello_erlang_server.erl, run a small +# rebar3 compile (incremental against the warmed _build), and execute a +# rebar3 eval that calls into the gen_server and prints the EXAM-HELLO +# canonical line. The snippet itself is a gen_server with no main entry +# point — gonfalon's flow expects the user to drop into rebar3 shell +# and call manually. We synthesize the equivalent at validate time. + +ARG LD_ERLANG_VERSION=3.9.0 + +WORKDIR /opt/hello_erlang + +RUN rebar3 new app hello_erlang \ + && mv hello_erlang/* hello_erlang/.??* . 2>/dev/null || true \ + && rmdir hello_erlang 2>/dev/null || true + +# Pin the SDK + add it to the app-src + supervisor child spec. We bake +# the same content the snippet's manifest-fragments would otherwise +# describe. +RUN printf '{erl_opts, [debug_info]}.\n\ +{deps, [\n\ + {ldclient, "%s", {pkg, launchdarkly_server_sdk}}\n\ +]}.\n\ +{shell, [\n\ + {apps, [hello_erlang]}\n\ +]}.\n' "${LD_ERLANG_VERSION}" > rebar.config + +RUN printf '{application, hello_erlang,\n\ + [{description, "Hello LaunchDarkly"},\n\ + {vsn, "0.1.0"},\n\ + {registered, []},\n\ + {mod, {hello_erlang_app, []}},\n\ + {applications,\n\ + [kernel,\n\ + stdlib,\n\ + ldclient\n\ + ]},\n\ + {env,[]},\n\ + {modules, []}\n\ + ]}.\n' > src/hello_erlang.app.src + +RUN cat > src/hello_erlang_sup.erl <<'EOF' +-module(hello_erlang_sup). +-behaviour(supervisor). +-export([start_link/0]). +-export([init/1]). +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +init([]) -> + SupFlags = #{strategy => one_for_all, intensity => 0, period => 1}, + ChildSpecs = [{console, + {hello_erlang_server, start_link, []}, + permanent, 5000, worker, [hello_erlang_server]}], + {ok, {SupFlags, ChildSpecs}}. +EOF + +# Placeholder server module — replaced per-validate. +RUN cat > src/hello_erlang_server.erl <<'EOF' +-module(hello_erlang_server). +-behaviour(gen_server). +-export([start_link/0, init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). +-export([get/3]). +start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +get(_, F, _) -> F. +init(_) -> {ok, []}. +handle_call(_, _, S) -> {reply, ok, S}. +handle_cast(_, S) -> {noreply, S}. +handle_info(_, S) -> {noreply, S}. +terminate(_, _) -> ok. +code_change(_, S, _) -> {ok, S}. +EOF + +RUN rebar3 compile + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/erlang-server/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/erlang-server/harness/run.sh b/snippets/validators/languages/erlang-server/harness/run.sh new file mode 100755 index 0000000..e876f59 --- /dev/null +++ b/snippets/validators/languages/erlang-server/harness/run.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Builds the staged Erlang snippet against the pre-baked rebar3 project, +# then runs `rebar3 eval` to start the gen_server, evaluate the flag, +# and print the EXAM-HELLO canonical line. +# +# The snippet is a gen_server module — the user-facing Get Started flow +# expects the user to launch `rebar3 shell` and call into it manually. +# For CI we synthesize the equivalent: ensure_all_started, sleep so the +# SDK has time to fetch flags, call hello_erlang_server:get/3, format +# the canonical line, halt. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_SDK_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +cp "/snippet/$SNIPPET_ENTRYPOINT" /opt/hello_erlang/src/hello_erlang_server.erl + +cd /opt/hello_erlang +rebar3 compile >/tmp/build.log 2>&1 \ + || { cat /tmp/build.log >&2; exit 1; } + +LOG=$(mktemp) + +# rebar3 doesn't ship `eval` in the bundled tasks, so drive `erl` +# directly. The compiled beams + transitive deps live under +# _build/default/lib/*/ebin. -noshell + -s init stop wraps the eval in +# a non-interactive session that exits when init:stop/0 fires. +EVAL_EXPR="application:ensure_all_started(hello_erlang), +timer:sleep(3000), +FlagKey = <<\"$LAUNCHDARKLY_FLAG_KEY\">>, +Result = hello_erlang_server:get(FlagKey, false, <<\"example-user-key\">>), +io:format(\"The ~s feature flag evaluates to ~p~n\", [FlagKey, Result]), +init:stop()." + +timeout --signal=TERM 60s erl \ + -pa _build/default/lib/*/ebin \ + -noshell \ + -eval "$EVAL_EXPR" >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 55 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/erlang-server/runner.yaml b/snippets/validators/languages/erlang-server/runner.yaml new file mode 100644 index 0000000..c933b30 --- /dev/null +++ b/snippets/validators/languages/erlang-server/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/erlang-server-validator From af4afea510415532dab1948a1d88996e2200eb2e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:00:20 -0700 Subject: [PATCH 18/19] feat(snippets): android-client-sdk validator (Robolectric, no emulator) Skip the Android emulator entirely: Robolectric runs the activity lifecycle in the JVM with stubbed Android framework + real OkHttp/network, which is enough to exercise the snippet's init+evaluate path against the live LaunchDarkly streaming API. Dockerfile clones hello-android (LD's official Get Started reference) for project scaffolding, patches app/build.gradle to pin launchdarkly-android-client-sdk@5.11.1 + Robolectric 4.12.2, and adds a HelloAppTest that: 1. Triggers MainApplication.onCreate via ApplicationProvider so LDClient.init runs with the snippet-baked mobile key. 2. Drives MainActivity through Robolectric's lifecycle controller. 3. Polls textView.text for the canonical EXAM-HELLO line, flushing the foreground looper between checks so the streaming SDK's flag listener can fire. Per-validate is just `gradlew testDebugUnitTest`. Total local time (warm gradle cache): ~45s. --- .github/workflows/snippets-validate.yml | 3 +- .../getting-started/main-activity.snippet.md | 6 +- .../languages/android-client/Dockerfile | 79 +++++++++++++++++++ .../languages/android-client/harness/run.sh | 37 +++++++++ .../languages/android-client/runner.yaml | 3 + .../android-client/test/HelloAppTest.kt | 58 ++++++++++++++ 6 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 snippets/validators/languages/android-client/Dockerfile create mode 100755 snippets/validators/languages/android-client/harness/run.sh create mode 100644 snippets/validators/languages/android-client/runner.yaml create mode 100644 snippets/validators/languages/android-client/test/HelloAppTest.kt diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 3d8d0d2..76fbbb8 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -64,9 +64,10 @@ jobs: - { sdk: vue-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: react-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: flutter-client-sdk, runs-on: ubuntu-latest, key-type: client } + - { sdk: android-client-sdk, runs-on: ubuntu-latest, key-type: mobile } # # Validators not yet wired (snippets ported, harness pending): - # Linux mobile/native: android-client-sdk, + # Linux mobile/native: # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) # Skipped: roku-client-sdk (validation: none, manual procedure). diff --git a/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md b/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md index 2f3852c..527b35b 100644 --- a/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md @@ -11,8 +11,10 @@ inputs: description: Default flag key baked into the rendered source. ld-application: slot: main-activity -# Validator pending — Android validation needs setup-android + a Linux -# emulator boot, slow but feasible. Deferred. +validation: + runtime: android-client + entrypoint: app/src/main/java/com/launchdarkly/hello_android/MainActivity.kt + companions: [android-client-sdk/getting-started/main-application] --- Open the file `MainActivity.kt` and add the following code: diff --git a/snippets/validators/languages/android-client/Dockerfile b/snippets/validators/languages/android-client/Dockerfile new file mode 100644 index 0000000..33d2c86 --- /dev/null +++ b/snippets/validators/languages/android-client/Dockerfile @@ -0,0 +1,79 @@ +FROM eclipse-temurin:17-jdk-noble + +# Android SDK command-line tools + the build-tools / platforms versions +# the snippet needs. We *don't* install the emulator: Robolectric runs +# the activity lifecycle in the JVM, which is enough to exercise the +# snippet's init+evaluate path against the real LD streaming API. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl unzip git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV ANDROID_HOME=/opt/android-sdk +ENV ANDROID_SDK_ROOT=${ANDROID_HOME} +ENV PATH="${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}" + +# Pin a specific cmdline-tools build to avoid surprise upgrades. +ARG CMDLINE_TOOLS_VERSION=11076708 +RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \ + && curl -fsSL --retry 3 --retry-delay 5 --max-time 600 "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" -o /tmp/tools.zip \ + && unzip -q /tmp/tools.zip -d ${ANDROID_HOME}/cmdline-tools \ + && mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \ + && rm /tmp/tools.zip + +RUN yes | sdkmanager --licenses >/dev/null \ + && sdkmanager --install "platforms;android-34" "platform-tools" "build-tools;34.0.0" + +# Pre-bake a hello-android gradle scaffold. We clone the upstream +# hello-android repo (LD's official getting-started reference) at a +# pinned ref, then patch app/build.gradle to use the latest SDK + +# Robolectric (so the validator runs in a pure JVM, no emulator). +ARG HELLO_ANDROID_REF=main +ARG LD_ANDROID_SDK_VERSION=5.11.1 +ARG ROBOLECTRIC_VERSION=4.12.2 + +RUN git clone --depth 1 --branch "${HELLO_ANDROID_REF}" \ + https://github.com/launchdarkly/hello-android.git /opt/hello-android + +WORKDIR /opt/hello-android + +# Patch the LD SDK version + add Robolectric to testImplementation. +RUN sed -i "s|com.launchdarkly:launchdarkly-android-client-sdk:[0-9.]*|com.launchdarkly:launchdarkly-android-client-sdk:${LD_ANDROID_SDK_VERSION}|" app/build.gradle \ + && sed -i "/^dependencies {$/a\\ testImplementation 'org.robolectric:robolectric:${ROBOLECTRIC_VERSION}'\\n testImplementation 'junit:junit:4.13.2'\\n testImplementation 'androidx.test:core:1.5.0'\\n testImplementation 'androidx.test.ext:junit:1.1.5'" app/build.gradle + +# Robolectric needs testOptions { unitTests.includeAndroidResources } to +# resolve R.string / R.id at test time. Forward stdout/stderr so the +# harness can grep the canonical EXAM-HELLO line; without this the +# textView dump only ends up in build/reports/tests. +RUN cat >> app/build.gradle <<'EOF' + +android { + testOptions { + unitTests { + includeAndroidResources = true + all { + testLogging { + events 'passed', 'failed', 'standardOut', 'standardError' + showStandardStreams = true + } + } + } + } +} +EOF + +# Drop a Robolectric test that drives the Activity lifecycle and reads +# the textView's content. This file is harness-internal; the snippet +# itself stays unchanged. +RUN mkdir -p app/src/test/java/com/launchdarkly/hello_android +COPY languages/android-client/test/HelloAppTest.kt \ + app/src/test/java/com/launchdarkly/hello_android/HelloAppTest.kt + +# Pre-warm: pull deps, compile, run a placeholder test cycle. +RUN ./gradlew --no-daemon dependencies --console=plain >/dev/null 2>&1 || true + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/android-client/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/android-client/harness/run.sh b/snippets/validators/languages/android-client/harness/run.sh new file mode 100755 index 0000000..d6dd545 --- /dev/null +++ b/snippets/validators/languages/android-client/harness/run.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Validates the Android snippet under Robolectric (no emulator). The +# Dockerfile pre-bakes a hello-android scaffold + Robolectric test; +# per-validate we just swap the snippet's two Kotlin files in and run +# the JUnit test. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_MOBILE_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +# The snippet declares main-application + main-activity as separate +# files. Both come through as part of the staging dir — copy whichever +# .kt files /snippet has into the scaffold's main source tree. +SCAFFOLD=/opt/hello-android +PKG_DIR="${SCAFFOLD}/app/src/main/java/com/launchdarkly/hello_android" + +for f in /snippet/app/src/main/java/com/launchdarkly/hello_android/*.kt; do + [ -f "$f" ] || continue + cp "$f" "${PKG_DIR}/$(basename "$f")" +done + +cd "${SCAFFOLD}" + +LOG=$(mktemp) +timeout --signal=TERM 600s ./gradlew --no-daemon \ + testDebugUnitTest --tests='*HelloAppTest*' --console=plain \ + >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 590 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/android-client/runner.yaml b/snippets/validators/languages/android-client/runner.yaml new file mode 100644 index 0000000..e1d1966 --- /dev/null +++ b/snippets/validators/languages/android-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/android-client-validator diff --git a/snippets/validators/languages/android-client/test/HelloAppTest.kt b/snippets/validators/languages/android-client/test/HelloAppTest.kt new file mode 100644 index 0000000..e8fe39d --- /dev/null +++ b/snippets/validators/languages/android-client/test/HelloAppTest.kt @@ -0,0 +1,58 @@ +package com.launchdarkly.hello_android + +import android.widget.TextView +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Robolectric test that drives MainApplication + MainActivity end-to-end + * against the real LaunchDarkly streaming API. This is the validator's + * harness; it lives in app/src/test/ and is added to the project at + * docker-build time. The snippet's two .kt files are dropped into + * app/src/main/ at validate time and compiled alongside this test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33], application = MainApplication::class) +class HelloAppTest { + @Test + fun flagEvaluatesToTrue() { + // ApplicationProvider triggers MainApplication.onCreate which + // calls LDClient.init with the mobile key the snippet baked in. + val app = ApplicationProvider.getApplicationContext() + check(app != null) { "MainApplication context not registered" } + + // Drive the activity lifecycle. MainActivity.onCreate calls + // boolVariation and renders into the TextView. + val controller = Robolectric.buildActivity(MainActivity::class.java) + .create() + .start() + .resume() + .visible() + val activity = controller.get() + val textView = activity.findViewById(R.id.textview) + + // Wait for the streaming SDK to fetch the flag and fire the + // change listener, then for Robolectric's main looper to apply + // the resulting setText. Polling with a wider deadline is + // robust against transient network jitter. + val deadline = System.currentTimeMillis() + 30_000 + var rendered = textView.text.toString() + while (System.currentTimeMillis() < deadline) { + rendered = textView.text.toString() + if (rendered.contains("evaluates to true", ignoreCase = true)) { + println("validator: ok") + println(rendered) + return + } + Thread.sleep(500) + org.robolectric.shadows.ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + } + assertTrue("did not see expected line; got: $rendered", + rendered.contains("evaluates to true", ignoreCase = true)) + } +} From 0d5c3a8131934685040714c382088280791d5632 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:34:57 -0700 Subject: [PATCH 19/19] feat(snippets): react-native-client-sdk validator (jest, no simulator) Skip Metro + iOS simulator + Android emulator entirely: jest with @react-native/jest-preset + @testing-library/react-native renders the snippet's component tree in a pure node environment, which is enough to exercise the SDK's init+evaluate path against the live LaunchDarkly backend. Two adjustments versus a vanilla setup: 1. AppState.currentState is undefined in the jest env; the SDK's RNStateDetector translates that to ApplicationState.Background, and with automaticBackgroundHandling+runInBackground=false (the SDK defaults), the connection manager immediately switches to offline mode. jest.setup.js pins AppState.currentState = 'active'. 2. The SDK's streaming transport (RNEventSource, internal in @launchdarkly/react-native-client-sdk) is implemented with XMLHttpRequest, which doesn't exist in Node. The test mocks ReactNativeLDClient via a subclass that forces initialConnectionMode='polling' so the SDK uses fetch (built into Node 18+) instead. Per-validate stages App.tsx + src/welcome.tsx and runs jest. ~30s on a warm cache (mostly polling roundtrip + jest startup). --- .github/workflows/snippets-validate.yml | 3 +- .../getting-started/app-tsx.snippet.md | 5 +- .../languages/react-native-client/Dockerfile | 119 ++++++++++++++++++ .../react-native-client/harness/run.sh | 31 +++++ .../languages/react-native-client/runner.yaml | 3 + .../react-native-client/test/App.test.tsx | 52 ++++++++ 6 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 snippets/validators/languages/react-native-client/Dockerfile create mode 100755 snippets/validators/languages/react-native-client/harness/run.sh create mode 100644 snippets/validators/languages/react-native-client/runner.yaml create mode 100644 snippets/validators/languages/react-native-client/test/App.test.tsx diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml index 76fbbb8..aabae7c 100644 --- a/.github/workflows/snippets-validate.yml +++ b/.github/workflows/snippets-validate.yml @@ -65,10 +65,9 @@ jobs: - { sdk: react-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: flutter-client-sdk, runs-on: ubuntu-latest, key-type: client } - { sdk: android-client-sdk, runs-on: ubuntu-latest, key-type: mobile } + - { sdk: react-native-client-sdk, runs-on: ubuntu-latest, key-type: mobile } # # Validators not yet wired (snippets ported, harness pending): - # Linux mobile/native: - # react-native-client-sdk (key-type: mobile) # macos-latest + xcodebuild: ios-client-sdk (key-type: mobile) # Skipped: roku-client-sdk (validation: none, manual procedure). steps: diff --git a/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md b/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md index cec80a5..606c912 100644 --- a/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -11,7 +11,10 @@ inputs: description: Mobile key baked into the rendered source. ld-application: slot: app-tsx -# Validator pending — RN bundler + Expo boot is heavy; deferred. +validation: + runtime: react-native-client + entrypoint: App.tsx + companions: [react-native-client-sdk/getting-started/welcome-tsx] --- In `App.tsx`: diff --git a/snippets/validators/languages/react-native-client/Dockerfile b/snippets/validators/languages/react-native-client/Dockerfile new file mode 100644 index 0000000..29cf5a9 --- /dev/null +++ b/snippets/validators/languages/react-native-client/Dockerfile @@ -0,0 +1,119 @@ +FROM node:22-bookworm + +# React Native's jest setup pulls in a long tail of transitive deps; +# pre-bake them so per-validate cycles only re-run jest with the +# snippet's two .tsx files. + +ARG LD_RN_SDK_VERSION=10.17.2 +ARG REACT_NATIVE_VERSION=0.85.2 +ARG REACT_VERSION=19.2.5 +ARG TESTING_LIBRARY_VERSION=13.3.3 + +WORKDIR /opt/hello-react-native + +# Minimal package.json — just enough for jest + RN testing-library to +# resolve the snippet's imports. Native code paths are mocked out by +# react-native's jest preset; the SDK's streaming layer talks to LD +# through the same JS code path it would use on a real device. +RUN cat > package.json </__tests__/**/*.test.tsx"], + "setupFiles": ["/jest.setup.js"] + } +} +EOF + +RUN cat > babel.config.js <<'EOF' +module.exports = { + presets: ['@react-native/babel-preset'], +}; +EOF + +# jest setup: react-native's preset leaves AppState.currentState as +# undefined, which the LD SDK's RNStateDetector translates to +# `Background`. With automaticBackgroundHandling=true (the default) and +# runInBackground=false (the default) the SDK then immediately enters +# offline mode, so the streaming connection never opens. Pin +# currentState to 'active' so the SDK treats us as foregrounded. +RUN cat > jest.setup.js <<'EOF' +const RN = require('react-native'); +if (RN.AppState) { + RN.AppState.currentState = 'active'; +} +EOF + +RUN cat > tsconfig.json <<'EOF' +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "jsx": "react-native", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": false + } +} +EOF + +# Placeholder snippet files — replaced per-validate. +RUN mkdir -p src __tests__ \ + && cat > App.tsx <<'EOF' +import React from 'react'; +import {Text, View} from 'react-native'; +const App = () => placeholder; +export default App; +EOF + +RUN cat > src/welcome.tsx <<'EOF' +import React from 'react'; +import {Text, View} from 'react-native'; +export default function Welcome() { + return placeholder; +} +EOF + +WORKDIR /opt/hello-react-native + +RUN npm install --no-audit --no-fund --no-progress + +# Drop a jest test that renders App and waits for the canonical line. +COPY languages/react-native-client/test/App.test.tsx __tests__/App.test.tsx + +WORKDIR /work + +COPY shared /harness-shared +COPY languages/react-native-client/harness /harness + +ENTRYPOINT ["/harness/run.sh"] diff --git a/snippets/validators/languages/react-native-client/harness/run.sh b/snippets/validators/languages/react-native-client/harness/run.sh new file mode 100755 index 0000000..2998c3d --- /dev/null +++ b/snippets/validators/languages/react-native-client/harness/run.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Validates the React Native snippet under jest with the react-native +# preset (no emulator/simulator). The Dockerfile pre-installed the +# bundled deps; per-validate just swaps the snippet's two .tsx files +# in and re-runs jest. +set -eu + +. /harness-shared/lib.sh +require_env LAUNCHDARKLY_MOBILE_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +PROJECT=/opt/hello-react-native + +# Stage App.tsx (root) + src/welcome.tsx (companion). Both are .tsx +# files at known paths under the snippet stage dir. +cp "/snippet/App.tsx" "${PROJECT}/App.tsx" +cp "/snippet/src/welcome.tsx" "${PROJECT}/src/welcome.tsx" + +cd "${PROJECT}" + +LOG=$(mktemp) +timeout --signal=TERM 180s npm test --silent >"$LOG" 2>&1 & +PID=$! + +deadline=$(( $(date +%s) + 170 )) +if await_success_line "$LOG" "$PID" "$deadline"; then + exit 0 +fi + +kill -TERM "$PID" 2>/dev/null || true +wait "$PID" 2>/dev/null || true +fail_with_log "$LOG" "did not see expected line: feature flag evaluates to true" diff --git a/snippets/validators/languages/react-native-client/runner.yaml b/snippets/validators/languages/react-native-client/runner.yaml new file mode 100644 index 0000000..ab46d7a --- /dev/null +++ b/snippets/validators/languages/react-native-client/runner.yaml @@ -0,0 +1,3 @@ +mode: docker +runs-on: ubuntu-latest +image-prefix: sdk-snippets/react-native-client-validator diff --git a/snippets/validators/languages/react-native-client/test/App.test.tsx b/snippets/validators/languages/react-native-client/test/App.test.tsx new file mode 100644 index 0000000..2e685bf --- /dev/null +++ b/snippets/validators/languages/react-native-client/test/App.test.tsx @@ -0,0 +1,52 @@ +// Jest test that drives the snippet's App.tsx + src/welcome.tsx end to +// end. Renders the React tree, waits up to 30s for the SDK to deliver +// the flag, then asserts that the rendered text contains the canonical +// EXAM-HELLO line. +// +// The SDK's default streaming transport uses XMLHttpRequest under the +// hood (RNEventSource), which doesn't exist in Node. Force polling +// mode (which uses fetch — built into Node 18+) so the test can talk +// to LaunchDarkly's polling endpoint and resolve the flag. +import React from 'react'; +import {render, waitFor, screen} from '@testing-library/react-native'; + +jest.mock('@launchdarkly/react-native-client-sdk', () => { + const actual = jest.requireActual('@launchdarkly/react-native-client-sdk'); + class PollingLDClient extends actual.ReactNativeLDClient { + constructor(key: string, autoEnv: any, options: any = {}) { + super(key, autoEnv, {...options, initialConnectionMode: 'polling'}); + } + } + return {...actual, ReactNativeLDClient: PollingLDClient}; +}); + +// eslint-disable-next-line import/first +import App from '../App'; + +jest.setTimeout(60_000); + +// Walks the rendered tree and concatenates every Text child into a +// single flat string. Needed because react-native's render output is a +// nested object — a regex looking for "feature flag evaluates to true" +// only matches when the words are adjacent. +function flattenText(node: any): string { + if (node == null) return ''; + if (typeof node === 'string') return node; + if (Array.isArray(node)) return node.map(flattenText).join(''); + if (typeof node === 'object' && node.children) return flattenText(node.children); + return ''; +} + +test('flag evaluates to true', async () => { + render(); + await waitFor( + () => { + const text = flattenText(screen.toJSON()); + expect(text).toMatch(/feature flag evaluates to true/i); + }, + {timeout: 30_000, interval: 500}, + ); + // Print the flat text so the validator harness's grep on + // "feature flag evaluates to [Tt]rue" matches. + console.log(flattenText(screen.toJSON())); +});