diff --git a/.github/workflows/snippets-validate.yml b/.github/workflows/snippets-validate.yml new file mode 100644 index 00000000..5b2ce16f --- /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_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 +# 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. 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. + +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: + # 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 } + - { 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 } + - { 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 } + - { 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 } + - { 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 } + # iOS (macOS runner + xcodebuild + iOS Simulator): + - { 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_EXAMPLES }} + command: | + set -o pipefail + 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 diff --git a/snippets/internal/adapters/ldapplication/descriptor.go b/snippets/internal/adapters/ldapplication/descriptor.go index 3a247cbc..541d0cc2 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 2d3307cf..3766c5b0 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/internal/render/render.go b/snippets/internal/render/render.go index 5c41b4c7..64bb8044 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 f32e43d4..d5d94717 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/android-client-sdk/sdk.yaml b/snippets/sdks/android-client-sdk/sdk.yaml new file mode 100644 index 00000000..c277670f --- /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 00000000..e1ecbbf2 --- /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 00000000..932da6fb --- /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 00000000..527b35bb --- /dev/null +++ b/snippets/sdks/android-client-sdk/snippets/getting-started/main-activity.snippet.md @@ -0,0 +1,84 @@ +--- +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 +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: + +```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 00000000..e43508e8 --- /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 00000000..3c9b82e6 --- /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 00000000..cd966ccb --- /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 00000000..e9bb45dc --- /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 00000000..7486068c --- /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 00000000..e2473335 --- /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 00000000..93bed588 --- /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 00000000..9cfb2044 --- /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 00000000..0438afa7 --- /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 00000000..4581b91b --- /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 00000000..e2662b17 --- /dev/null +++ b/snippets/sdks/cpp-client-sdk/snippets/getting-started/main-cpp.snippet.md @@ -0,0 +1,75 @@ +--- +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 +validation: + runtime: cpp-client + entrypoint: main.cpp +--- + +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 << "*** The '{{ featureKey }}' feature flag evaluates to " + << (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 00000000..1a404b08 --- /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 00000000..f12c72f5 --- /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 00000000..86710bd8 --- /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 00000000..fe98206d --- /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 00000000..5b3ccb95 --- /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 00000000..2814c8c2 --- /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 00000000..f800ffd6 --- /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 00000000..1d58e627 --- /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 00000000..28919f1c --- /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 00000000..7a488871 --- /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 00000000..350ca092 --- /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 +validation: + runtime: cpp-server + entrypoint: main.cpp +--- + +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 << "*** The '{{ featureKey }}' feature flag evaluates to " + << (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 00000000..55463d97 --- /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 00000000..899d43bb --- /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 00000000..850af30f --- /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 00000000..ad8bbd2a --- /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 00000000..d5aa58f8 --- /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 00000000..1eff91ce --- /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 00000000..1124862a --- /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("example-user-key"); +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 00000000..e00f1a46 --- /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 00000000..46cf7ca1 --- /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 00000000..bb14a5b9 --- /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 00000000..c5e7fa1f --- /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 00000000..a9fe6f91 --- /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 00000000..cf2ab953 --- /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 00000000..a52a673f --- /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 00000000..84262f5a --- /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 00000000..e11a41ed --- /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 00000000..c0ff2b70 --- /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 00000000..90866939 --- /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 00000000..8260420f --- /dev/null +++ b/snippets/sdks/erlang-server-sdk/snippets/getting-started/server-erl.snippet.md @@ -0,0 +1,68 @@ +--- +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. +ld-application: + slot: server-erl +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 +`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 00000000..aa78a7b5 --- /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 00000000..db5e2c15 --- /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 00000000..4881325d --- /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 00000000..18ca0076 --- /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 00000000..9cdd497f --- /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 00000000..bb3758fc --- /dev/null +++ b/snippets/sdks/flutter-client-sdk/snippets/getting-started/main-dart.snippet.md @@ -0,0 +1,137 @@ +--- +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 +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: + +```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 00000000..6796620b --- /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 00000000..dadbe180 --- /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 00000000..52dc2a5b --- /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 00000000..647cb5b6 --- /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 00000000..46f76301 --- /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 00000000..eb4dc950 --- /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 00000000..6ad5d593 --- /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 00000000..e631974d --- /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 00000000..3de14355 --- /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 00000000..69d7d0f0 --- /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 00000000..eab83924 --- /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: haskell-server + entrypoint: app/Main.hs +--- + +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 "LAUNCHDARKLY_FLAG_KEY" + 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 00000000..3c2195df --- /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 00000000..7b341e3a --- /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 00000000..ae0decfb --- /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 00000000..99b0e63c --- /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 00000000..8d2e9bb0 --- /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 00000000..bba1fbcf --- /dev/null +++ b/snippets/sdks/ios-client-sdk/snippets/getting-started/app-delegate.snippet.md @@ -0,0 +1,50 @@ +--- +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 +--- + +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 00000000..bc7e2878 --- /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 00000000..e715c00b --- /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 '11.1.2' as a fallback when gonfalon's async fetch from CocoaPods hasn't completed. + runtime-default: "11.1.2" +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 00000000..65b49ab7 --- /dev/null +++ b/snippets/sdks/ios-client-sdk/snippets/getting-started/view-controller.snippet.md @@ -0,0 +1,56 @@ +--- +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 +validation: + runtime: ios-client + entrypoint: ViewController.swift + companions: [ios-client-sdk/getting-started/app-delegate] +--- + +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 00000000..1382d621 --- /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 00000000..c5fcdc52 --- /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 00000000..1fdcf453 --- /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 00000000..b418180d --- /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 00000000..f46f095a --- /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 00000000..9d5229c1 --- /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 00000000..bad92d36 --- /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 00000000..cd32a8e6 --- /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 00000000..d1aded9b --- /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 00000000..8e29703c --- /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 00000000..6f53ad34 --- /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 00000000..a7f0a893 --- /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 00000000..e3c32853 --- /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 +validation: + runtime: lua-server + entrypoint: hello.lua +--- + +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("*** The '{{ featureKey }}' feature flag evaluates to "..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 00000000..6e68f9c1 --- /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 00000000..b30e2ebe --- /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 00000000..bf402bb8 --- /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 00000000..3f7e834a --- /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 00000000..2fa48059 --- /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 00000000..7d852056 --- /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 00000000..f6681858 --- /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 00000000..56251c30 --- /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 00000000..dd78648f --- /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 00000000..0c39e6be --- /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 00000000..cf618c86 --- /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 00000000..15decb8f --- /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 00000000..4d18f668 --- /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 00000000..d6d0893d --- /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 00000000..ffad0111 --- /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 00000000..68ea1e99 --- /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 00000000..6687980d --- /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 00000000..449df516 --- /dev/null +++ b/snippets/sdks/react-client-sdk/sdk.yaml @@ -0,0 +1,23 @@ +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: + # 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 new file mode 100644 index 00000000..d3764986 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -0,0 +1,36 @@ +--- +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 +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`: + +```tsx +import { useFlags } from 'launchdarkly-react-client-sdk'; + +function App() { + const { {{ featureKey | camelCase }} } = useFlags(); + + return ( +
+ The {{ featureKey | camelCase }} feature flag evaluates to { {{ featureKey | camelCase }} ? '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 00000000..f83e807b --- /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 00000000..d1199c93 --- /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 00000000..d0ea28cf --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/legacy-app-tsx.snippet.md @@ -0,0 +1,39 @@ +--- +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 +validation: + runtime: react-client + entrypoint: src/App.tsx + companions: [react-client-sdk/getting-started/legacy-index-tsx] +--- + +In `App.tsx`: + +```tsx +import './App.css'; +import { useFlags } from 'launchdarkly-react-client-sdk'; + +function App() { + const { {{ featureKey | camelCase }} } = useFlags(); + + return ( +
+
+

The {{ featureKey | camelCase }} feature flag evaluates to { {{ featureKey | camelCase }} ? '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 00000000..5ea5c831 --- /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 00000000..1727c20b --- /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 00000000..d2b074c7 --- /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 00000000..f2d78b69 --- /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 00000000..29b1e1d0 --- /dev/null +++ b/snippets/sdks/react-client-sdk/snippets/getting-started/main-tsx.snippet.md @@ -0,0 +1,34 @@ +--- +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 +--- + +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 00000000..50cdd763 --- /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 00000000..21c4938a --- /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 00000000..606c912a --- /dev/null +++ b/snippets/sdks/react-native-client-sdk/snippets/getting-started/app-tsx.snippet.md @@ -0,0 +1,52 @@ +--- +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 +validation: + runtime: react-native-client + entrypoint: App.tsx + companions: [react-native-client-sdk/getting-started/welcome-tsx] +--- + +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 00000000..950d3985 --- /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 00000000..b6776869 --- /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 00000000..04fc0903 --- /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 00000000..d66d45c9 --- /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 00000000..694416be --- /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 00000000..e43ca87d --- /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 00000000..7415b4d5 --- /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 00000000..b9e524aa --- /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 00000000..bfeed364 --- /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 00000000..1228d448 --- /dev/null +++ b/snippets/sdks/vue-client-sdk/snippets/getting-started/main-js.snippet.md @@ -0,0 +1,29 @@ +--- +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 +--- + +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-key' }, +}) +app.mount('#app') +``` diff --git a/snippets/validators/languages/android-client/Dockerfile b/snippets/validators/languages/android-client/Dockerfile new file mode 100644 index 00000000..33d2c86b --- /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 00000000..d6dd545f --- /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 00000000..e1d19667 --- /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 00000000..e8fe39dc --- /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)) + } +} diff --git a/snippets/validators/languages/browser/Dockerfile b/snippets/validators/languages/browser/Dockerfile new file mode 100644 index 00000000..f182dd29 --- /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 00000000..115b8e8a --- /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 00000000..e3966629 --- /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 00000000..2cbdbf5d --- /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/cpp-client/Dockerfile b/snippets/validators/languages/cpp-client/Dockerfile new file mode 100644 index 00000000..57880493 --- /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 00000000..87e26948 --- /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 00000000..74e722dd --- /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 diff --git a/snippets/validators/languages/cpp-server/Dockerfile b/snippets/validators/languages/cpp-server/Dockerfile new file mode 100644 index 00000000..594ef4fa --- /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 00000000..820aa3bf --- /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 00000000..b5c0a215 --- /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 diff --git a/snippets/validators/languages/dotnet-client/Dockerfile b/snippets/validators/languages/dotnet-client/Dockerfile new file mode 100644 index 00000000..d4f0e63c --- /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 00000000..b0f29d7b --- /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 00000000..6867bf81 --- /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 00000000..debf5a1e --- /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 00000000..0561e5ab --- /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 00000000..117c59e5 --- /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/erlang-server/Dockerfile b/snippets/validators/languages/erlang-server/Dockerfile new file mode 100644 index 00000000..ddc1d444 --- /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 00000000..e876f59e --- /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 00000000..c933b308 --- /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 diff --git a/snippets/validators/languages/flutter-client/Dockerfile b/snippets/validators/languages/flutter-client/Dockerfile new file mode 100644 index 00000000..ecf95bbf --- /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 00000000..36f0fb56 --- /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 00000000..d4b2835a --- /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 00000000..d54b6809 --- /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 diff --git a/snippets/validators/languages/go/Dockerfile b/snippets/validators/languages/go/Dockerfile new file mode 100644 index 00000000..7f21abba --- /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 00000000..d3b76941 --- /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 00000000..b23424af --- /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/haskell-server/Dockerfile b/snippets/validators/languages/haskell-server/Dockerfile new file mode 100644 index 00000000..3df10098 --- /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 00000000..30082221 --- /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 00000000..ee8ff76e --- /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 diff --git a/snippets/validators/languages/ios-client/harness/run.sh b/snippets/validators/languages/ios-client/harness/run.sh new file mode 100755 index 00000000..8a9b7d27 --- /dev/null +++ b/snippets/validators/languages/ios-client/harness/run.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Validates the iOS snippet on macos-latest with xcodebuild + iOS +# Simulator. The snippet's AppDelegate + ViewController are dropped +# into a pre-baked Xcode project (generated from the scaffold's +# project.yml via xcodegen), pointed at the launchdarkly-ios-client-sdk +# Swift Package, and exercised via an XCTest case. +# +# `mode: native` — xcodebuild + iOS Simulator don't run inside Linux +# containers, so the CI cell sets runs-on: macos-latest. +set -eu + +# The runner doesn't mount /harness-shared (no docker), so source the +# helpers via a relative path. +. "$(cd "$(dirname "$0")/../../../shared" && pwd)/lib.sh" + +require_env LAUNCHDARKLY_MOBILE_KEY LAUNCHDARKLY_FLAG_KEY SNIPPET_ENTRYPOINT + +SCAFFOLD="$(cd "$(dirname "$0")/../scaffold" && pwd)" + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +cp -R "$SCAFFOLD"/. "$WORK"/ +cp "$SNIPPET_DIR/AppDelegate.swift" "$WORK/Sources/AppDelegate.swift" +cp "$SNIPPET_DIR/ViewController.swift" "$WORK/Sources/ViewController.swift" + +cd "$WORK" + +if ! command -v xcodegen >/dev/null 2>&1; then + brew install xcodegen +fi +xcodegen generate + +# Pick whichever iPhone simulator is currently installed on this +# runner. macos-latest's iPhone roster shifts per Xcode release — +# hardcoding "iPhone 15" stops working when GitHub bumps the image. +SIM_NAME=$(xcrun simctl list devices available --json | python3 -c ' +import sys, json, re +data = json.load(sys.stdin) +best = None +best_num = -1 +for runtime, devs in data.get("devices", {}).items(): + if "iOS" not in runtime: + continue + for dev in devs: + name = dev.get("name", "") + if not dev.get("isAvailable", False): + continue + if not name.startswith("iPhone"): + continue + # Prefer plain "iPhone " over Pro/Max/Plus variants so the + # destination string is shortest and most likely to match. + m = re.match(r"^iPhone (\d+)$", name) + if m and int(m.group(1)) > best_num: + best_num = int(m.group(1)) + best = name +if best is None: + # Fallback: any available iPhone, max version-suffix wins. + cand = [] + for runtime, devs in data.get("devices", {}).items(): + if "iOS" not in runtime: + continue + for dev in devs: + if dev.get("isAvailable") and dev.get("name", "").startswith("iPhone"): + cand.append(dev["name"]) + best = sorted(cand)[-1] if cand else "iPhone 16" +print(best) +') +DESTINATION="platform=iOS Simulator,name=$SIM_NAME" +echo "validator: targeting $DESTINATION" + +LOG=$(mktemp) + +# `-resolvePackageDependencies` is an action mutually exclusive with +# `test`; xcodebuild silently runs only the resolve when both are +# passed. Resolve packages first, then run the test action. +xcodebuild -resolvePackageDependencies \ + -project HelloIOS.xcodeproj \ + -scheme HelloIOS \ + >>"$LOG" 2>&1 + +set +e +SIMCTL_CHILD_LAUNCHDARKLY_MOBILE_KEY="$LAUNCHDARKLY_MOBILE_KEY" \ +SIMCTL_CHILD_LAUNCHDARKLY_FLAG_KEY="$LAUNCHDARKLY_FLAG_KEY" \ +xcodebuild test \ + -project HelloIOS.xcodeproj \ + -scheme HelloIOS \ + -destination "$DESTINATION" \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGN_IDENTITY="" \ + >>"$LOG" 2>&1 +XCB_EXIT=$? +set -e + +if grep -q "feature flag evaluates to true" "$LOG"; then + grep -E "feature flag evaluates to true|validator: rendered" "$LOG" | head -3 + echo "validator: ok" + exit 0 +fi + +echo "validator: did not see expected line: feature flag evaluates to true (xcodebuild exit=$XCB_EXIT)" >&2 +echo "--- last 100 lines of xcodebuild output ---" >&2 +tail -100 "$LOG" >&2 +exit 1 diff --git a/snippets/validators/languages/ios-client/runner.yaml b/snippets/validators/languages/ios-client/runner.yaml new file mode 100644 index 00000000..8ae21967 --- /dev/null +++ b/snippets/validators/languages/ios-client/runner.yaml @@ -0,0 +1,2 @@ +mode: native +runs-on: macos-latest diff --git a/snippets/validators/languages/ios-client/scaffold/Sources/AppDelegate.swift b/snippets/validators/languages/ios-client/scaffold/Sources/AppDelegate.swift new file mode 100644 index 00000000..4b6c8a97 --- /dev/null +++ b/snippets/validators/languages/ios-client/scaffold/Sources/AppDelegate.swift @@ -0,0 +1,11 @@ +// Placeholder — replaced per-validate by the staged snippet. +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } +} diff --git a/snippets/validators/languages/ios-client/scaffold/Sources/Info.plist b/snippets/validators/languages/ios-client/scaffold/Sources/Info.plist new file mode 100644 index 00000000..f7eebf76 --- /dev/null +++ b/snippets/validators/languages/ios-client/scaffold/Sources/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/snippets/validators/languages/ios-client/scaffold/Sources/ViewController.swift b/snippets/validators/languages/ios-client/scaffold/Sources/ViewController.swift new file mode 100644 index 00000000..9a5a52a7 --- /dev/null +++ b/snippets/validators/languages/ios-client/scaffold/Sources/ViewController.swift @@ -0,0 +1,6 @@ +// Placeholder — replaced per-validate by the staged snippet. +import UIKit + +class ViewController: UIViewController { + @IBOutlet weak var featureFlagLabel: UILabel! +} diff --git a/snippets/validators/languages/ios-client/scaffold/Tests/SnippetTest.swift b/snippets/validators/languages/ios-client/scaffold/Tests/SnippetTest.swift new file mode 100644 index 00000000..513d31b5 --- /dev/null +++ b/snippets/validators/languages/ios-client/scaffold/Tests/SnippetTest.swift @@ -0,0 +1,57 @@ +// Drives the snippet's AppDelegate + ViewController against the live +// LaunchDarkly streaming API. The host app's AppDelegate.didFinishLaunching +// has already called LDClient.start(...) by the time this test runs. +import XCTest +import LaunchDarkly +@testable import HelloIOS + +final class SnippetTest: XCTestCase { + func testFlagEvaluatesToTrue() throws { + let flagKey = ProcessInfo.processInfo.environment["LAUNCHDARKLY_FLAG_KEY"] + ?? "sample-feature" + + guard let ld = LDClient.get() else { + XCTFail("LDClient.get() returned nil — AppDelegate.setUpLDClient never ran") + return + } + + // The AppDelegate uses startWaitSeconds: 30, so by the time + // didFinishLaunching returns the SDK has either initialized or + // timed out. Poll boolVariation a few extra seconds in case the + // first event hasn't been processed yet. + let exp = expectation(description: "flag fetched") + var lastResult = false + let observer = NSObject() + ld.observe(key: flagKey, owner: observer) { changedFlag in + if case .bool(let b) = changedFlag.newValue, b { + lastResult = true + exp.fulfill() + } + } + // Also seed with the current value in case streaming has already + // delivered the flag before observe() registered. + if ld.boolVariation(forKey: flagKey, defaultValue: false) { + lastResult = true + exp.fulfill() + } + wait(for: [exp], timeout: 30.0) + + // Drive the snippet's ViewController through the same code path + // gonfalon shows the user. The IBOutlet is wired manually so + // updateUi can write into a real label. + let vc = ViewController() + let label = UILabel() + vc.featureFlagLabel = label + vc.loadViewIfNeeded() + vc.viewDidLoad() + // viewDidLoad is async-ish — let one runloop spin so the + // observer callback has a chance to fire. + RunLoop.main.run(until: Date(timeIntervalSinceNow: 1.0)) + + let rendered = label.text ?? "" + print("validator: rendered=\(rendered)") + XCTAssertTrue(lastResult, "expected flag to evaluate to true") + XCTAssertTrue(rendered.lowercased().contains("feature flag evaluates to true"), + "expected canonical line in label, got: \(rendered)") + } +} diff --git a/snippets/validators/languages/ios-client/scaffold/project.yml b/snippets/validators/languages/ios-client/scaffold/project.yml new file mode 100644 index 00000000..bd7cae15 --- /dev/null +++ b/snippets/validators/languages/ios-client/scaffold/project.yml @@ -0,0 +1,61 @@ +name: HelloIOS +options: + bundleIdPrefix: com.launchdarkly + deploymentTarget: + iOS: "16.0" + createIntermediateGroups: true + +# launchdarkly-ios-client-sdk via Swift Package Manager — avoids the +# CocoaPods step described in the snippet for the user. We're testing +# the snippet's runtime behavior, not the dependency manager itself. +packages: + LaunchDarkly: + url: https://github.com/launchdarkly/ios-client-sdk + from: "11.0.0" + +targets: + HelloIOS: + type: application + platform: iOS + sources: + - path: Sources + info: + path: Sources/Info.plist + properties: + UILaunchStoryboardName: "" + CFBundleName: HelloIOS + CFBundleIdentifier: com.launchdarkly.HelloIOS + CFBundleVersion: "1" + CFBundleShortVersionString: "1.0" + dependencies: + - package: LaunchDarkly + settings: + base: + SWIFT_VERSION: "5.9" + PRODUCT_BUNDLE_IDENTIFIER: com.launchdarkly.HelloIOS + TARGETED_DEVICE_FAMILY: "1,2" + CODE_SIGNING_ALLOWED: "NO" + CODE_SIGN_IDENTITY: "" + + HelloIOSTests: + type: bundle.unit-test + platform: iOS + sources: + - path: Tests + dependencies: + - target: HelloIOS + settings: + base: + SWIFT_VERSION: "5.9" + CODE_SIGNING_ALLOWED: "NO" + CODE_SIGN_IDENTITY: "" + +schemes: + HelloIOS: + build: + targets: + HelloIOS: all + HelloIOSTests: [test] + test: + targets: + - HelloIOSTests diff --git a/snippets/validators/languages/jvm/Dockerfile b/snippets/validators/languages/jvm/Dockerfile new file mode 100644 index 00000000..00243ba6 --- /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 00000000..ffb22611 --- /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 00000000..dd9c93a4 --- /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/lua-server/Dockerfile b/snippets/validators/languages/lua-server/Dockerfile new file mode 100644 index 00000000..8ff74759 --- /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 00000000..0ab100d0 --- /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 00000000..b10379b0 --- /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 diff --git a/snippets/validators/languages/node/Dockerfile b/snippets/validators/languages/node/Dockerfile new file mode 100644 index 00000000..26049285 --- /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 00000000..bad2dc9a --- /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 00000000..e12f95d4 --- /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 00000000..ac42d4f5 --- /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 00000000..6e56fdd4 --- /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 00000000..bce9adf7 --- /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/react-client/Dockerfile b/snippets/validators/languages/react-client/Dockerfile new file mode 100644 index 00000000..698d2a90 --- /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 00000000..e1acf348 --- /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 00000000..99b6544a --- /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 00000000..a910da4c --- /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 diff --git a/snippets/validators/languages/react-native-client/Dockerfile b/snippets/validators/languages/react-native-client/Dockerfile new file mode 100644 index 00000000..29cf5a92 --- /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 00000000..2998c3dc --- /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 00000000..ab46d7a2 --- /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 00000000..2e685bfa --- /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())); +}); diff --git a/snippets/validators/languages/ruby/Dockerfile b/snippets/validators/languages/ruby/Dockerfile new file mode 100644 index 00000000..24e55d2d --- /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 00000000..d110ed24 --- /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 00000000..1a285ce6 --- /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 00000000..9da7958a --- /dev/null +++ b/snippets/validators/languages/rust/Dockerfile @@ -0,0 +1,8 @@ +FROM rust:1.85 + +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 00000000..3c882499 --- /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 00000000..3eef4759 --- /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 diff --git a/snippets/validators/languages/vue-client/Dockerfile b/snippets/validators/languages/vue-client/Dockerfile new file mode 100644 index 00000000..015b7e5a --- /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 00000000..8d137e5d --- /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 00000000..12698ab0 --- /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 00000000..c6f452b3 --- /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