diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..7d7a7a00 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is the LaunchDarkly Integration Framework — a collection of 130+ third-party integration definitions. Integrations are declarative (JSON manifests + Handlebars templates), not imperative code. The framework validates manifests against a JSON Schema and renders templates with sample contexts in tests. + +## Commands + +- **Run all tests:** `npm test` +- **Run a single test file:** `npx jest __tests__/validateIntegrationManifests.js` +- **Run tests matching a pattern:** `npx jest -t "Validate datadog/manifest.json"` +- **Build schema bundle:** `npm run build` (bundles `schemas/base.json` + `schemas/definitions.json` + `schemas/capabilities/*.json` into `manifest.schema.json`, then generates TypeScript types) +- **Format code:** `npm run prettier:write` +- **Check formatting:** `npm run prettier:check` +- **Preview template rendering:** `npm run preview` (renders templates with sample context data) +- **Scaffold new integration:** `npx plop` (interactive generator using `plopfile.js`) +- **Validation server:** `npm run start:server` (Express server on port 3000 for testing integrations) + +## Architecture + +### Integration structure + +Each integration lives in `integrations//` with: +- `manifest.json` — declares metadata, form variables, capabilities, and endpoint configuration +- `assets/square.svg` and `assets/horizontal.svg` — logo files (required) +- `templates/*.json.hbs` — Handlebars templates for different event types (flag, project, environment, member) +- `README.md` — optional documentation + +### Manifest schema + +The manifest schema is composed from multiple files: +- `schemas/base.json` — top-level manifest properties (name, version, overview, formVariables, capabilities, etc.) +- `schemas/definitions.json` — shared type definitions +- `schemas/capabilities/*.json` — per-capability schemas (auditLogEventsHook, trigger, flagLink, syncedSegment, approval, etc.) +- `manifest.schema.json` — auto-generated bundled/dereferenced schema (do not edit directly) +- `manifest.schema.d.ts` — auto-generated TypeScript types from the schema + +### Capabilities + +Integrations declare capabilities in their manifest. Key capability types: +- `auditLogEventsHook` / `eventsHook` — webhook-style event delivery with templated request bodies +- `trigger` — LaunchDarkly triggers +- `flagLink` — deep links to external resources from flags +- `syncedSegment` — import audience segments from external tools +- `approval` — external approval workflows +- `flagCleanup` / `flagImport` — flag lifecycle management +- `reservedCustomProperties` — custom flag properties for the integration + +### Handlebars templates + +Templates use Handlebars with custom helpers registered in `helpers/index.js`: +- `equal` / `equalWithElse` — conditional equality +- `pathEncode` / `queryEncode` — URL encoding +- `basicAuthHeaderValue` — Base64 auth header +- `formatWithOffset` — timestamp formatting (milliseconds, seconds, rfc3339, simple) + +Templates rendering is tested with sample contexts from `sample-context/` (flag, project, environment, member events). JSON templates are automatically escaped via `utils/json-escape.js`. + +### Test structure + +Tests in `__tests__/` validate all integrations automatically: +- Schema validation of every manifest against `manifest.schema.json` +- Icon file existence checks +- Overview must end with a period +- Template rendering with sample contexts must not throw +- Form variable references in endpoints/templates must resolve +- Capability-specific validations (flag links, synced segments, etc.) + +Tests iterate over all integration directories using `__tests__/__utils__/index.js` which reads every `integrations/*/manifest.json`. + +## Key conventions + +- Manifests are validated with AJV against the bundled JSON Schema +- Template variables come from `formVariables` defined in the manifest +- The `overview` field must end with a period (enforced by tests) +- OAuth integrations must be explicitly listed in `OAUTH_INTEGRATIONS` array in `__tests__/validateIntegrationManifests.js` +- After modifying schemas, run `npm run build` to regenerate `manifest.schema.json` diff --git a/integrations/bigquery-experimentation/assets/images/horizontal.svg b/integrations/bigquery-experimentation/assets/images/horizontal.svg new file mode 100644 index 00000000..d3b56405 --- /dev/null +++ b/integrations/bigquery-experimentation/assets/images/horizontal.svg @@ -0,0 +1,212 @@ + + + + + + image/svg+xml + + integration-tile + + + + + + + + integration-tile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrations/bigquery-experimentation/assets/images/square.svg b/integrations/bigquery-experimentation/assets/images/square.svg new file mode 100644 index 00000000..76e46050 --- /dev/null +++ b/integrations/bigquery-experimentation/assets/images/square.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/integrations/bigquery-experimentation/manifest.json b/integrations/bigquery-experimentation/manifest.json new file mode 100644 index 00000000..bc5fe6c9 --- /dev/null +++ b/integrations/bigquery-experimentation/manifest.json @@ -0,0 +1,115 @@ +{ + "name": "BigQuery Native Experimentation", + "version": "1.0.0", + "overview": "Run experiments in LaunchDarkly using data in your BigQuery warehouse to power your results.", + "description": "Enable LaunchDarkly's experimentation platform to read and analyze data from your Google BigQuery data warehouse", + "author": "LaunchDarkly", + "supportEmail": "support@launchdarkly.com", + "allowIntegrationConfigurations": true, + "links": { + "site": "https://cloud.google.com/bigquery", + "launchdarklyDocs": "https://launchdarkly.com/docs/home/warehouse-native/bigquery", + "privacyPolicy": "https://launchdarkly.com/policies/privacy/" + }, + "categories": ["data"], + "icons": { + "square": "assets/images/square.svg", + "horizontal": "assets/images/horizontal.svg" + }, + "otherCapabilities": ["warehouseExperimentation"], + "formVariables": [ + { + "key": "selectedEnv", + "type": "environmentSelector", + "name": "Project and environment", + "description": "Select your LaunchDarkly project and environment. You cannot edit this after you save the configuration.", + "disableAfterSaving": true, + "isOptional": false + }, + { + "key": "gcpProjectId", + "name": "GCP project ID", + "description": "The Google Cloud project that holds your BigQuery resources.", + "type": "string", + "disableAfterSaving": true, + "isSecret": false, + "isOptional": false + }, + { + "key": "datasetLocation", + "name": "Dataset location", + "description": "BigQuery region for your datasets (for example US or EU). All datasets must be in the same location.", + "type": "string", + "disableAfterSaving": true, + "isSecret": false, + "isOptional": false + }, + { + "key": "resultsDatasetId", + "name": "Results dataset ID", + "description": "Dataset where LaunchDarkly writes experimentation results.", + "type": "string", + "disableAfterSaving": true, + "isSecret": false, + "isOptional": false + }, + { + "key": "exportDatasetId", + "name": "Data Export dataset ID", + "description": "The dataset where your BigQuery Data Export destination writes event data.", + "type": "string", + "disableAfterSaving": true, + "isSecret": false, + "isOptional": false + }, + { + "key": "exportProjectId", + "name": "Export project ID", + "description": "Optional backend-only export project override.", + "type": "string", + "isHidden": true, + "disableAfterSaving": true, + "isSecret": false, + "isOptional": true, + "defaultValue": "" + }, + { + "key": "metricsDatasetId", + "name": "Metrics dataset ID (optional)", + "description": "Optional metrics dataset for dataset-level access instead of project-level read access.", + "type": "string", + "disableAfterSaving": true, + "isSecret": false, + "isOptional": true, + "defaultValue": "" + }, + { + "key": "metricsProjectId", + "name": "Metrics project ID", + "description": "Optional backend-only metrics project override.", + "type": "string", + "isHidden": true, + "disableAfterSaving": true, + "isSecret": false, + "isOptional": true, + "defaultValue": "" + }, + { + "key": "serviceAccountKey", + "name": "Service account key (JSON)", + "description": "Paste the JSON key for the service account created by the setup script.", + "type": "string", + "isSecret": true, + "isOptional": false + }, + { + "key": "recurringTasksId", + "name": "Recurring Task ID", + "description": "The task id of the scheduled work task for this integration.", + "type": "string", + "isHidden": true, + "isOptional": true, + "defaultValue": "" + } + ] +} diff --git a/integrations/chronosphere/manifest.json b/integrations/chronosphere/manifest.json index 3af64d67..13265255 100644 --- a/integrations/chronosphere/manifest.json +++ b/integrations/chronosphere/manifest.json @@ -27,7 +27,8 @@ "key": "receiverURL", "name": "Chronosphere receiver URL", "description": "Your Chronosphere receiver URL. Should look like https://{MY_COMPANY}.chronosphere.io/api/v1/data/events/receiver/launchdarkly", - "type": "string" + "type": "string", + "clearSecretsOnChange": true } ], "capabilities": { diff --git a/integrations/clickhouse/assets/images/horizontal.svg b/integrations/clickhouse/assets/images/horizontal.svg new file mode 100644 index 00000000..2e30277f --- /dev/null +++ b/integrations/clickhouse/assets/images/horizontal.svg @@ -0,0 +1,8 @@ + + + + + + +ClickHouse + diff --git a/integrations/clickhouse/assets/images/square.svg b/integrations/clickhouse/assets/images/square.svg new file mode 100644 index 00000000..51e1ee30 --- /dev/null +++ b/integrations/clickhouse/assets/images/square.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/integrations/clickhouse/manifest.json b/integrations/clickhouse/manifest.json new file mode 100644 index 00000000..af3ad0ee --- /dev/null +++ b/integrations/clickhouse/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "ClickHouse Data Export", + "version": "1.0.0", + "overview": "Run analysis in your warehouse enriched by experiment traffic data or detailed flag eval events.", + "description": "Export LaunchDarkly data to your ClickHouse warehouse.", + "author": "ClickHouse", + "supportEmail": "support@launchdarkly.com", + "links": { + "site": "https://clickhouse.com/", + "launchdarklyDocs": "https://launchdarkly.com/docs/integrations/data-export", + "privacyPolicy": "https://launchdarkly.com/policies/privacy" + }, + "categories": ["data"], + "icons": { + "square": "assets/images/square.svg", + "horizontal": "assets/images/horizontal.svg" + }, + "legacy": { + "kind": "dataExport" + }, + "otherCapabilities": ["dataExport", "warehouseExport"] +} diff --git a/integrations/compass/manifest.json b/integrations/compass/manifest.json index cd5ffc0b..58269e3a 100644 --- a/integrations/compass/manifest.json +++ b/integrations/compass/manifest.json @@ -22,7 +22,8 @@ "name": "Compass web trigger URL", "type": "uri", "description": "Enter your Compass web trigger URL", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "secret", diff --git a/integrations/convex/manifest.json b/integrations/convex/manifest.json index e9553ea9..7af480c1 100644 --- a/integrations/convex/manifest.json +++ b/integrations/convex/manifest.json @@ -23,7 +23,8 @@ "description": "Enter the URL to the webhook exposed by the LaunchDarkly component for Convex.", "type": "uri", "placeholder": "https://techno-kitten-138.convex.site/ld/webhook", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "componentApiToken", diff --git a/integrations/custom-approvals/manifest.json b/integrations/custom-approvals/manifest.json index 3e310dc1..674bac88 100644 --- a/integrations/custom-approvals/manifest.json +++ b/integrations/custom-approvals/manifest.json @@ -30,7 +30,8 @@ "name": "Custom approval service base URL", "description": "The base URL of your custom approval service. This is where all of the API handlers should be set up.", "type": "string", - "placeholder": "e.g. https://mycustomapprovalservice.com" + "placeholder": "e.g. https://mycustomapprovalservice.com", + "clearSecretsOnChange": true } ], "allowIntegrationConfigurations": true, diff --git a/integrations/databricks-experimentation/manifest.json b/integrations/databricks-experimentation/manifest.json index 00eb7d08..8277ee9f 100644 --- a/integrations/databricks-experimentation/manifest.json +++ b/integrations/databricks-experimentation/manifest.json @@ -19,25 +19,34 @@ "otherCapabilities": ["warehouseExperimentation"], "formVariables": [ { - "key": "stepConnect", + "key": "selectedEnv", + "type": "environmentSelector", + "name": "Project and environment", + "description": "This cannot be edited after this destination is saved.", + "disableAfterSaving": true, + "isOptional": false + }, + { + "key": "configureConnection", "type": "sectionHeading", - "name": "Step 1: Connect to Databricks", - "description": "Enter your Databricks workspace and SQL warehouse connection details.", + "name": "Configure connection", + "description": "", "divider": true }, { "key": "databricksHost", - "name": "Databricks workspace URL", - "description": "The URL of your Databricks workspace (e.g., https://your-workspace.cloud.databricks.com)", + "name": "Databricks workspace host", + "description": "Enter the URL of your Databricks workspace. Raw host name is sufficient", "placeholder": "https://your-workspace.cloud.databricks.com", "type": "string", "disableAfterSaving": true, - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "httpPath", - "name": "SQL warehouse HTTP path", - "description": "The HTTP path to your SQL warehouse (e.g., /sql/1.0/warehouses/abc123)", + "name": "Warehouse HTTP path", + "description": "Enter the HTTP path of your Databricks SQL warehouse. Eg /sql/1.0/warehouses/abc123", "placeholder": "/sql/1.0/warehouses/abc123", "type": "string", "disableAfterSaving": true, @@ -45,37 +54,15 @@ }, { "key": "accessToken", - "name": "Service principal access token", - "description": "The access token for your Databricks service principal", + "name": "Personal Access token", + "description": "Personal Access Token (PAT), which should match the desired Data Export integration.", "type": "string", "isSecret": true }, - { - "key": "stepEnv", - "type": "sectionHeading", - "name": "Step 2: Choose LaunchDarkly environment", - "description": "Select the LaunchDarkly project and environment that will use this Databricks connection.", - "divider": true - }, - { - "key": "selectedEnv", - "type": "environmentSelector", - "name": "Project and environment", - "description": "Select your LaunchDarkly project and environment. You cannot edit this after you save the configuration.", - "disableAfterSaving": true, - "isOptional": false - }, - { - "key": "stepStorage", - "type": "sectionHeading", - "name": "Step 3: Data storage", - "description": "Specify where your event data is stored and where LaunchDarkly should write experiment results.", - "divider": true - }, { "key": "catalog", - "name": "Unity Catalog catalog name", - "description": "The name of the Unity Catalog catalog containing your data", + "name": "Catalog name", + "description": "Provide the Unity catalog name where LaunchDarkly data will be stored", "placeholder": "main", "type": "string", "disableAfterSaving": true, @@ -83,61 +70,33 @@ }, { "key": "schema", - "name": "Schema name for results", - "description": "The schema name where experiment results will be stored", + "name": "Schema name", + "description": "Schema name where computed tables will be created", "placeholder": "experimentation", "type": "string", "disableAfterSaving": true, "isSecret": false }, - { - "key": "stepMetrics", - "type": "sectionHeading", - "name": "Step 4: Metrics settings", - "description": "Configure where metric events are stored in Databricks.", - "divider": true - }, { "key": "metricsCatalog", - "name": "Metrics catalog name", - "description": "The Databricks catalog where metric events are stored", + "name": "Metrics database name", + "description": "Database where metric events are stored in Databricks", "placeholder": "metrics", "type": "string", - "isSecret": false + "isSecret": false, + "isOptional": true, + "defaultValue": "" }, { "key": "metricsSchema", "name": "Metrics schema name", - "description": "The schema within the metrics catalog (leave blank for all schemas)", + "description": "If not provided, access is granted to all schemas of the catalog entered above", "placeholder": "events", "type": "string", "isSecret": false, "isOptional": true, "defaultValue": "" }, - { - "key": "stepSql", - "type": "sectionHeading", - "name": "Step 5: Run SQL setup", - "description": "Run the SQL script below to set up required objects in Databricks.", - "divider": true - }, - { - "key": "sqlSetupScript", - "type": "codeEditor", - "name": "SQL setup script", - "description": "Copy and run this script in Databricks to create the required objects for the integration.", - "codeLanguage": "sql", - "instructionsHeading": "How to run this script", - "instructions": [ - "In Databricks, open a SQL editor attached to the warehouse you entered above.", - "Copy the script below and run it.", - "If you update catalog/schema values, re-run the script." - ], - "hideEmpty": true, - "isOptional": true, - "defaultValue": "" - }, { "key": "recurringTasksId", "name": "Recurring Task Id", diff --git a/integrations/datadog-private/manifest.json b/integrations/datadog-private/manifest.json index 2cab26fd..8cf15bf7 100644 --- a/integrations/datadog-private/manifest.json +++ b/integrations/datadog-private/manifest.json @@ -28,7 +28,8 @@ "name": "Datadog host URL", "description": "Your Datadog host URL. Read [How do I tell which Datadog site I am on?](https://docs.datadoghq.com/getting_started/site/#how-do-i-tell-which-datadog-site-i-am-on) if you are unsure which host URL to select.", "type": "string", - "placeholder": "https://api.datadoghq.com" + "placeholder": "https://api.datadoghq.com", + "clearSecretsOnChange": true }, { "key": "hideMemberDetails", diff --git a/integrations/dynatrace-v2/manifest.json b/integrations/dynatrace-v2/manifest.json index 816703ac..87dc1bbf 100644 --- a/integrations/dynatrace-v2/manifest.json +++ b/integrations/dynatrace-v2/manifest.json @@ -29,7 +29,8 @@ "type": "uri", "description": "Enter the URL used to access your Dynatrace (managed or hosted) service. Follow the pattern shown in the placeholder text.", "placeholder": "https://{your-environment-id}.live.dynatrace.com", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "entity", diff --git a/integrations/dynatrace/manifest.json b/integrations/dynatrace/manifest.json index 6c5ce309..f3d680ea 100644 --- a/integrations/dynatrace/manifest.json +++ b/integrations/dynatrace/manifest.json @@ -29,7 +29,8 @@ "type": "uri", "description": "Enter the URL used to access your Dynatrace (managed or hosted) service. Follow the pattern shown in the placeholder text.", "placeholder": "https://{your-environment-id}.live.dynatrace.com", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "entity", diff --git a/integrations/elastic/manifest.json b/integrations/elastic/manifest.json index 45da59b1..0f687948 100644 --- a/integrations/elastic/manifest.json +++ b/integrations/elastic/manifest.json @@ -22,6 +22,7 @@ "description": "Enter the URL for your Elasticsearch endpoint, including the socket", "type": "uri", "isSecret": false, + "clearSecretsOnChange": true, "placeholder": "https://cluster.region.aws.found.io:9243", "isOptional": false }, diff --git a/integrations/grafana/manifest.json b/integrations/grafana/manifest.json index 0d3df03a..ce09477c 100644 --- a/integrations/grafana/manifest.json +++ b/integrations/grafana/manifest.json @@ -22,7 +22,8 @@ "description": "Enter your Grafana instance's URL. This instance must be accessible to LaunchDarkly's servers. Do not include a trailing \"/\".", "placeholder": "https://grafana.example.org", "type": "uri", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "apiKey", diff --git a/integrations/jira/manifest.json b/integrations/jira/manifest.json index 86405a99..c27cd89d 100644 --- a/integrations/jira/manifest.json +++ b/integrations/jira/manifest.json @@ -22,7 +22,8 @@ "name": "Jira web trigger URL", "type": "uri", "description": "Enter your Jira web trigger URL", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "secret", diff --git a/integrations/kosli/manifest.json b/integrations/kosli/manifest.json index 8ff30464..4c9b84df 100644 --- a/integrations/kosli/manifest.json +++ b/integrations/kosli/manifest.json @@ -21,7 +21,8 @@ "key": "webhookUrl", "name": "Kosli Webhook URL", "type": "string", - "description": "Enter your Kosli webhook URL" + "description": "Enter your Kosli webhook URL", + "clearSecretsOnChange": true }, { "key": "secret", diff --git a/integrations/last9/manifest.json b/integrations/last9/manifest.json index e1a6f27b..d1ee3b59 100644 --- a/integrations/last9/manifest.json +++ b/integrations/last9/manifest.json @@ -22,7 +22,8 @@ "description": "Enter your Last9 api base url. Do not include a trailing \"/\".", "placeholder": "https://{host}/api/v4/organizations/{org}", "type": "uri", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "apiToken", diff --git a/integrations/ld-to-git/manifest.json b/integrations/ld-to-git/manifest.json index 791983b4..63bedf85 100644 --- a/integrations/ld-to-git/manifest.json +++ b/integrations/ld-to-git/manifest.json @@ -21,6 +21,7 @@ "description": "Updater Lambda endpoint where audit log would be forwarded", "type": "uri", "isSecret": false, + "clearSecretsOnChange": true, "isOptional": false }, { @@ -37,6 +38,7 @@ "description": "Enter the URL for your target repository.", "type": "uri", "isSecret": false, + "clearSecretsOnChange": true, "placeholder": "https://github.com/launchdarkly", "isOptional": false }, diff --git a/integrations/msteams-app/manifest.json b/integrations/msteams-app/manifest.json index 7e0c08a1..ed6dd139 100644 --- a/integrations/msteams-app/manifest.json +++ b/integrations/msteams-app/manifest.json @@ -21,7 +21,8 @@ "name": "Incoming webhook URL", "type": "uri", "description": "Enter your Microsoft Teams webhook URL", - "isSecret": false + "isSecret": false, + "clearSecretsOnChange": true }, { "key": "secret", diff --git a/integrations/redis/manifest.json b/integrations/redis/manifest.json index c8a23a17..f8deccb8 100644 --- a/integrations/redis/manifest.json +++ b/integrations/redis/manifest.json @@ -20,7 +20,8 @@ "key": "host", "name": "Host", "description": "Your Redis host.", - "type": "string" + "type": "string", + "clearSecretsOnChange": true }, { "key": "port", diff --git a/integrations/redshift-experimentation/assets/images/horizontal.svg b/integrations/redshift-experimentation/assets/images/horizontal.svg new file mode 100644 index 00000000..b789a59d --- /dev/null +++ b/integrations/redshift-experimentation/assets/images/horizontal.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Redshiftct_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrations/redshift-experimentation/assets/images/square.svg b/integrations/redshift-experimentation/assets/images/square.svg new file mode 100644 index 00000000..af15ffcf --- /dev/null +++ b/integrations/redshift-experimentation/assets/images/square.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/32/Arch_Amazon-Redshift_32 + + + + + + + \ No newline at end of file diff --git a/integrations/redshift-experimentation/manifest.json b/integrations/redshift-experimentation/manifest.json new file mode 100644 index 00000000..1041c6f9 --- /dev/null +++ b/integrations/redshift-experimentation/manifest.json @@ -0,0 +1,229 @@ +{ + "name": "Redshift Native Experimentation", + "version": "1.0.0", + "overview": "Run experiments in LaunchDarkly using Redshift warehouse data to power your results.", + "description": "Enable LaunchDarkly's experimentation platform to read and analyze data from your Amazon Redshift data warehouse", + "author": "LaunchDarkly", + "supportEmail": "support@launchdarkly.com", + "allowIntegrationConfigurations": true, + "links": { + "site": "https://aws.amazon.com/redshift/", + "launchdarklyDocs": "https://launchdarkly.com/docs/home/warehouse-native/redshift", + "privacyPolicy": "https://launchdarkly.com/policies/privacy/" + }, + "categories": ["data"], + "icons": { + "square": "assets/images/square.svg", + "horizontal": "assets/images/horizontal.svg" + }, + "otherCapabilities": ["warehouseExperimentation"], + "formVariables": [ + { + "key": "selectedEnv", + "type": "environmentSelector", + "name": "Project and environment", + "description": "Select your LaunchDarkly project and environment. You cannot edit this after you save the configuration.", + "disableAfterSaving": true, + "isOptional": false + }, + { + "key": "step1Heading", + "type": "sectionHeading", + "name": "Step 1: Add Endpoint and Cluster Identifier", + "description": "Enter the connection details for your Amazon Redshift cluster.", + "isOptional": true, + "defaultValue": "", + "divider": true + }, + { + "key": "clusterEndpoint", + "name": "Redshift endpoint", + "description": "The endpoint URL for your Redshift cluster (e.g., my-cluster.abc123.us-east-1.redshift.amazonaws.com).", + "placeholder": "my-cluster.abc123.us-east-1.redshift.amazonaws.com", + "type": "string", + "disableAfterSaving": true, + "isSecret": false + }, + { + "key": "clusterIdentifier", + "name": "Redshift cluster identifier", + "description": "The unique identifier of your Amazon Redshift cluster.", + "placeholder": "my-redshift-cluster", + "type": "string", + "disableAfterSaving": true, + "isSecret": false + }, + { + "key": "clusterRegion", + "name": "Redshift cluster region", + "description": "The region of your Amazon Redshift cluster.", + "placeholder": "us-east-1", + "type": "string", + "disableAfterSaving": true, + "isSecret": false + }, + { + "key": "clusterAwsAccountId", + "name": "Redshift cluster AWS account ID", + "description": "The AWS account ID of your Amazon Redshift cluster.", + "placeholder": "123456789012", + "type": "string", + "disableAfterSaving": true, + "isSecret": false + }, + { + "key": "step2Heading", + "type": "sectionHeading", + "name": "Step 2: Create IAM policy and IAM role", + "description": "Create the required AWS IAM resources to allow LaunchDarkly to connect to your Redshift cluster.", + "isOptional": true, + "defaultValue": "", + "divider": true + }, + { + "key": "iamPolicyJson", + "name": "IAM policy", + "description": "This policy gives LaunchDarkly permission to read experimentation data from your Redshift cluster. Follow Amazon's documentation to create the required IAM policy.", + "type": "codeEditor", + "codeLanguage": "json", + "instructionsHeading": "Continue setup in AWS IAM Console for Policies", + "instructions": [ + "Log into AWS and access the IAM section", + "Select Policies from the left sidebar", + "Click create policy", + "Select the create policy JSON option", + "Paste the above permissions policy into the policy document field and save the policy" + ], + "isOptional": true, + "defaultValue": "" + }, + { + "key": "iamTrustPolicyJson", + "name": "IAM role", + "description": "Follow Amazon's documentation to create an IAM role that LaunchDarkly will assume to perform the actions defined in your IAM policy from the previous step.", + "type": "codeEditor", + "codeLanguage": "json", + "instructionsHeading": "Continue setup in AWS IAM Console for Roles", + "instructions": [ + "Log into AWS and access the IAM section", + "Select Roles from the left sidebar", + "Click create role", + "Select custom trust policy, paste the provided trust policy and click next", + "Add the permissions policy created above, and save this role", + "Copy the ARN of the role and paste it in the IAM Role ARN field below" + ], + "isOptional": true, + "defaultValue": "" + }, + { + "key": "iamRoleArn", + "name": "AWS IAM role ARN", + "description": "The ARN of the IAM role that LaunchDarkly will assume to connect to your Redshift cluster.", + "placeholder": "arn:aws:iam::123456789012:role/LaunchDarklyRedshiftRole", + "type": "string", + "disableAfterSaving": true, + "isSecret": false + }, + { + "key": "step3Heading", + "type": "sectionHeading", + "name": "Step 3: Add metric details", + "description": "Create the required AWS IAM resources to allow LaunchDarkly to connect to your Redshift cluster.", + "isOptional": true, + "defaultValue": "", + "divider": true + }, + { + "key": "metricsDatabaseName", + "name": "Metrics database name", + "description": "The name of your Redshift database where metrics events are stored.", + "placeholder": "analytics", + "type": "string", + "disableAfterSaving": true, + "isSecret": false + }, + { + "key": "metricsSchemaName", + "name": "Metrics schema name", + "description": "The name of the schema within your metrics database.", + "placeholder": "metric_events", + "type": "string", + "disableAfterSaving": true, + "isOptional": true, + "isSecret": false, + "defaultValue": "" + }, + { + "key": "step4Heading", + "type": "sectionHeading", + "name": "Step 4: Execute SQL script", + "description": "Continue setup in Redshift by running the script to prepare your Redshift account for native experimentation, then return to this page. This will create your DB, schema, user and grant permissions for experimentation.", + "isOptional": true, + "defaultValue": "", + "divider": true + }, + { + "key": "sqlSetupScriptPart1", + "name": "Part 1: Create DB, user and groups", + "description": "", + "type": "string", + "isOptional": true, + "defaultValue": "", + "instructionsHeading": "Connect to any database and run this script to create database, user and groups for experimentation" + }, + { + "key": "sqlSetupScriptPart2", + "name": "Part 2: Grant ld_experimentation permssions", + "description": "", + "type": "string", + "isOptional": true, + "defaultValue": "", + "instructionsHeading": "Connect to ld_experimentation database and run this script to grant permissions for experimentation" + }, + { + "key": "sqlSetupScriptPart3", + "name": "Part 3: Grant permissions to ld_export database", + "description": "", + "type": "string", + "isOptional": true, + "defaultValue": "", + "instructionsHeading": "Connect to ld_export database and run this script to grant permissions for experimentation" + }, + { + "key": "sqlSetupScriptPart4", + "name": "Part 4: Grant permissions to metrics database", + "description": "", + "type": "string", + "isOptional": true, + "defaultValue": "", + "instructionsHeading": "Connect to your metrics database and run this script to grant permissions for experimentation" + }, + { + "key": "syncTaskId", + "name": "Sync Task Id", + "description": "The task id of the sync task", + "type": "string", + "isHidden": true, + "isOptional": true, + "defaultValue": "" + }, + { + "key": "eventTaskId", + "name": "Event Last Seen Task Id", + "description": "The task id of the event last seen task", + "type": "string", + "isHidden": true, + "isOptional": true, + "defaultValue": "" + }, + { + "key": "recurringTasksId", + "name": "Recurring Task Id", + "description": "The task id of the scheduled work task for this integration", + "type": "string", + "isHidden": true, + "isOptional": true, + "defaultValue": "" + } + ] +} diff --git a/integrations/segment-audiences/assets/images/horizontal.svg b/integrations/segment-audiences/assets/images/horizontal.svg index 1949c758..0e48ca90 100644 --- a/integrations/segment-audiences/assets/images/horizontal.svg +++ b/integrations/segment-audiences/assets/images/horizontal.svg @@ -1,25 +1 @@ - - - - -2019_SegmentLogo_Horizontal - - - - - - - - - - - - - - - - - - + diff --git a/integrations/segment-audiences/assets/images/square.svg b/integrations/segment-audiences/assets/images/square.svg index 7b54e7a2..e1ca8786 100644 --- a/integrations/segment-audiences/assets/images/square.svg +++ b/integrations/segment-audiences/assets/images/square.svg @@ -1 +1,8 @@ - + + + + + + + + \ No newline at end of file diff --git a/integrations/segment-audiences/manifest.json b/integrations/segment-audiences/manifest.json index 93d2a073..b22e8900 100644 --- a/integrations/segment-audiences/manifest.json +++ b/integrations/segment-audiences/manifest.json @@ -1,8 +1,8 @@ { - "name": "Segment Audiences", + "name": "Twilio Segment Audiences", "version": "1.0.0", - "overview": "Sync Segment Audiences to LaunchDarkly Big Segments.", - "description": "Segment syncing lets you import audiences from Segment to LaunchDarkly to more efficiently target and deliver feature flags.", + "overview": "Sync Twilio Segment audiences to LaunchDarkly Big Segments.", + "description": "Twilio Segment audience syncing lets you import audiences from Twilio Segment to LaunchDarkly to more efficiently target and deliver feature flags.", "author": "LaunchDarkly", "supportEmail": "support@launchdarkly.com", "links": { diff --git a/integrations/segment-inbound/assets/images/horizontal.svg b/integrations/segment-inbound/assets/images/horizontal.svg index 1949c758..0e48ca90 100644 --- a/integrations/segment-inbound/assets/images/horizontal.svg +++ b/integrations/segment-inbound/assets/images/horizontal.svg @@ -1,25 +1 @@ - - - - -2019_SegmentLogo_Horizontal - - - - - - - - - - - - - - - - - - + diff --git a/integrations/segment-inbound/assets/images/square.svg b/integrations/segment-inbound/assets/images/square.svg index 7b54e7a2..e1ca8786 100644 --- a/integrations/segment-inbound/assets/images/square.svg +++ b/integrations/segment-inbound/assets/images/square.svg @@ -1 +1,8 @@ - + + + + + + + + \ No newline at end of file diff --git a/integrations/segment-inbound/manifest.json b/integrations/segment-inbound/manifest.json index 8dfb2561..01432406 100644 --- a/integrations/segment-inbound/manifest.json +++ b/integrations/segment-inbound/manifest.json @@ -1,8 +1,8 @@ { - "name": "Segment Inbound Events", + "name": "Twilio Segment Inbound Events", "version": "1.0.0", - "overview": "Send Segment events to LaunchDarkly experiments as metrics.", - "description": "Use Segment events as custom metric events in LaunchDarkly experiments, so you can measure results immediately, without any instrumentation, code, or delays.", + "overview": "Send Twilio Segment events to LaunchDarkly experiments as metrics.", + "description": "Use Twilio Segment events as custom metric events in LaunchDarkly experiments, so you can measure results immediately, without any instrumentation, code, or delays.", "author": "LaunchDarkly", "supportEmail": "support@launchdarkly.com", "links": { diff --git a/integrations/segment/assets/images/horizontal.svg b/integrations/segment/assets/images/horizontal.svg index 1949c758..0e48ca90 100644 --- a/integrations/segment/assets/images/horizontal.svg +++ b/integrations/segment/assets/images/horizontal.svg @@ -1,25 +1 @@ - - - - -2019_SegmentLogo_Horizontal - - - - - - - - - - - - - - - - - - + diff --git a/integrations/segment/assets/images/square.svg b/integrations/segment/assets/images/square.svg index 7b54e7a2..e1ca8786 100644 --- a/integrations/segment/assets/images/square.svg +++ b/integrations/segment/assets/images/square.svg @@ -1 +1,8 @@ - + + + + + + + + \ No newline at end of file diff --git a/integrations/segment/manifest.json b/integrations/segment/manifest.json index 30a95e82..4e5a5991 100644 --- a/integrations/segment/manifest.json +++ b/integrations/segment/manifest.json @@ -1,8 +1,8 @@ { - "name": "Segment", + "name": "Twilio Segment", "version": "1.0.0", - "overview": "Export real-time flag analytics data.", - "description": "Export your real-time flag analytics data to Segment.", + "overview": "Export real-time flag analytics data to Twilio Segment.", + "description": "Export your real-time flag analytics data to Twilio Segment.", "author": "LaunchDarkly", "supportEmail": "support@launchdarkly.com", "links": { diff --git a/integrations/servicenow-c0/manifest.json b/integrations/servicenow-c0/manifest.json index f8b77b57..22668db6 100644 --- a/integrations/servicenow-c0/manifest.json +++ b/integrations/servicenow-c0/manifest.json @@ -56,7 +56,8 @@ "key": "host", "name": "ServiceNow host", "description": "Your ServiceNow host", - "type": "string" + "type": "string", + "clearSecretsOnChange": true }, { "key": "template", diff --git a/integrations/servicenow-c1/assets/images/servicenow-horizontal.svg b/integrations/servicenow-c1/assets/images/servicenow-horizontal.svg new file mode 100644 index 00000000..4c76a79d --- /dev/null +++ b/integrations/servicenow-c1/assets/images/servicenow-horizontal.svg @@ -0,0 +1 @@ + diff --git a/integrations/servicenow-c1/assets/images/servicenow-icon.svg b/integrations/servicenow-c1/assets/images/servicenow-icon.svg new file mode 100644 index 00000000..f676f2c1 --- /dev/null +++ b/integrations/servicenow-c1/assets/images/servicenow-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/integrations/servicenow-c1/manifest.json b/integrations/servicenow-c1/manifest.json new file mode 100644 index 00000000..73504da1 --- /dev/null +++ b/integrations/servicenow-c1/manifest.json @@ -0,0 +1,187 @@ +{ + "name": "ServiceNow C1", + "version": "1.0.0", + "overview": "Create feature flag change requests in ServiceNow.", + "description": "Efficiently comply with company-wide change management policies by embedding LaunchDarkly approvals into existing ServiceNow workflows.", + "author": "LaunchDarkly", + "supportEmail": "support@launchdarkly.com", + "links": { + "site": "https://www.servicenow.com/", + "launchdarklyDocs": "https://launchdarkly.com/docs/integrations/servicenow", + "privacyPolicy": "https://www.servicenow.com/privacy-statement.html" + }, + "categories": ["approval"], + "icons": { + "square": "assets/images/servicenow-icon.svg", + "horizontal": "assets/images/servicenow-horizontal.svg" + }, + "formVariables": [], + "capabilities": { + "reservedCustomProperties": [ + { + "name": "ServiceNow app ID", + "key": "servicenowAppId" + }, + { + "name": "ServiceNow assignment group", + "key": "servicenowAssignmentGroup" + } + ], + "approval": { + "approvalFormVariables": [ + { + "key": "assignmentGroup", + "name": "ServiceNow assignment group override (Disabled)", + "description": "This request will fail if you have not specified the \"ServiceNow assignment group\" custom property. Read [Custom Properties](https://launchdarkly.com/docs/home/infrastructure/custom-properties) for more information.", + "type": "enum", + "allowedValues": ["Override disabled"], + "isOptional": true + } + ], + "environmentFormVariables": [ + { + "key": "username", + "name": "ServiceNow username", + "description": "Your ServiceNow integration user's username", + "type": "string" + }, + { + "key": "password", + "name": "ServiceNow password", + "description": "Your ServiceNow integration user's password", + "type": "string", + "isSecret": true + }, + { + "key": "host", + "name": "ServiceNow host", + "description": "Your ServiceNow host", + "type": "string", + "clearSecretsOnChange": true + }, + { + "key": "template", + "name": "Change request template", + "description": "The change request template to be utilized for all flag changes in this environment.", + "type": "string" + } + ], + "creationRequest": { + "endpoint": { + "url": "{{environment.host}}/api/now/import/u_create_change", + "method": "POST", + "headers": [ + { + "name": "Authorization", + "value": "{{{basicAuthHeaderValue environment.username environment.password}}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "jsonBody": "{ \"assignment_group\": \"{{{customProperties.servicenowAssignmentGroup.values.[0]}}}\", \"u_requesting_group\": \"{{{customProperties.servicenowAssignmentGroup.values.[0]}}}\", \"u_creating_group\": \"{{{customProperties.servicenowAssignmentGroup.values.[0]}}}\", \"opened_by\": \"{{{member.emailLocalPart}}}\", \"requested_by\": \"{{{member.emailLocalPart}}}\", \"assigned_to\": \"{{{member.emailLocalPart}}}\", \"description\": \"\\nBusiness Justification: Updating {{flag.name}} flag for {{customProperties.servicenowAppId.values.[0]}} Application.\\n\\nProject: {{{project.name}}} ({{project.key}})\\n{{{details.plainText}}}\\nRequested By: {{member.displayName}}\\nEmail Address: {{member.email}}\\n\\nLaunchDarkly Approval Request: {{{_links.approval.href}}}\\nChange History: {{{_links.history.href}}}\", \"short_description\": \"Updating Feature Flag for {{customProperties.servicenowAppId.values.[0]}}\", \"u_template\": \"{{environmentFormVariables.template}}\", \"start_date\": \"{{formatWithOffset timestamp.milliseconds 5 'simple'}}\", \"end_date\": \"{{formatWithOffset timestamp.milliseconds 300 'simple'}}\", \"u_uuid\": \"ld-{{timestamp.milliseconds}}\", \"approval\": \"Requested for Approval\", \"u_affected_ci\": \"{{{customProperties.servicenowAppId.values.[0]}}}\"}", + "parser": { + "approvalId": "/result/0/sys_id", + "statusValue": "/result/0/status", + "statusDisplay": "/result/0/status", + "approvalMatcher": "inserted", + "rejectionMatcher": "error", + "urlTemplate": "{{environment.host}}/itsmhome/?id=itsm_form&sys_id={{queryEncode context.approvalId}}&table=change_request" + } + }, + "statusRequest": { + "endpoint": { + "url": "{{environment.host}}/api/now/table/change_request?sysparm_query=active%3Dtrue%5Eapproval%3Dapproved%5Estate%3D2%5Enumber%3D{{context.approvalId}}&sysparm_fields=number%2Cstate%2Capproval&sysparm_limit=2", + "method": "GET", + "headers": [ + { + "name": "Authorization", + "value": "{{{basicAuthHeaderValue environment.username environment.password}}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "parser": { + "statusValue": "/result/0/state", + "statusDisplay": "/result/0/approval", + "approvalMatcher": "^2", + "rejectionMatcher": "x" + } + }, + "postApplyRequest": { + "endpoint": { + "url": "{{environment.host}}/api/now/import/u_update_change", + "method": "POST", + "headers": [ + { + "name": "Authorization", + "value": "{{{basicAuthHeaderValue environment.username environment.password}}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "jsonBody": "{ \"number\": \"{{{creationResponseBody.result.[0].display_value}}}\", \"requested_by\": \"{{{member.emailLocalPart}}}\", \"state\": \"Closed Complete\", \"u_close_task_state\": \"Closed Complete,Closed Complete,Closed Complete,Closed Complete\", \"u_closure_code\": \"Successful\", \"u_close_tasks\": \"{{{creationResponseBody.result.[0].AllTasks}}}\", \"u_closure_comments\": \"Closed by LaunchDarkly: {{{comment}}}\", \"u_uuid\": \"ld-{{timestamp.milliseconds}}\" }", + "parser": { + "statusValue": "/result/0/status", + "statusDisplay": "/result/0/status", + "approvalMatcher": "x", + "rejectionMatcher": "x" + } + }, + "deletionRequest": { + "endpoint": { + "url": "{{environment.host}}/api/now/change/cancel_change_request", + "method": "POST", + "headers": [ + { + "name": "Authorization", + "value": "{{{basicAuthHeaderValue environment.username environment.password}}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "jsonBody": "{ \"number\": \"{{{creationResponseBody.result.[0].display_value}}}\",\"requested_by\": \"{{{member.emailLocalPart}}}\", \"closure_comments\": \"The approval request was deleted in LaunchDarkly. No changes were made.\", \"u_uuid\": \"ld-{{timestamp.milliseconds}}\" }", + "parser": { + "statusValue": "/state", + "statusDisplay": "/state", + "approvalMatcher": "x", + "rejectionMatcher": "x" + } + }, + "memberListRequest": { + "endpoint": { + "url": "{environment.host}}/api/now/table/sys_user?active=true&sysparm_fields=email,user_name", + "method": "GET", + "headers": [ + { + "name": "Authorization", + "value": "{{{basicAuthHeaderValue environment.username environment.password}}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "parser": { + "memberArrayPath": "/result", + "memberItems": { + "memberId": "/user_name", + "email": "/email" + } + } + } + } + } +} diff --git a/integrations/snowflake-experimentation/manifest.json b/integrations/snowflake-experimentation/manifest.json index 06be9139..d22e6569 100644 --- a/integrations/snowflake-experimentation/manifest.json +++ b/integrations/snowflake-experimentation/manifest.json @@ -25,7 +25,9 @@ "placeholder": "https://..snowflakecomputing.com", "type": "string", "disableAfterSaving": true, - "isSecret": false + "isSecret": false, + "isOptional": false, + "clearSecretsOnChange": true }, { "key": "selectedEnv", @@ -35,6 +37,30 @@ "disableAfterSaving": true, "isOptional": false }, + { + "key": "metricsDatabaseName", + "name": "Metrics database name", + "description": "The Snowflake database containing your metric events. Used to generate the setup script that grants LaunchDarkly access to your metrics data.", + "placeholder": "MY_METRICS_DATABASE", + "type": "string", + "disableAfterSaving": true, + "isHidden": true, + "isSecret": false, + "isOptional": true, + "defaultValue": "" + }, + { + "key": "metricsSchemaName", + "name": "Metrics schema name (optional)", + "description": "The schema within the metrics database. If omitted, LaunchDarkly will be granted access to all schemas in the database.", + "placeholder": "PUBLIC", + "type": "string", + "disableAfterSaving": true, + "isHidden": true, + "isSecret": false, + "isOptional": true, + "defaultValue": "" + }, { "key": "publicKey", "name": "Public key", diff --git a/integrations/splunk/manifest.json b/integrations/splunk/manifest.json index bf6f5322..f187eb7c 100644 --- a/integrations/splunk/manifest.json +++ b/integrations/splunk/manifest.json @@ -22,6 +22,7 @@ "type": "string", "description": "Enter your [Splunk HTTP event collector base URL](https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector) (omitting the path).", "isSecret": false, + "clearSecretsOnChange": true, "placeholder": "https://http-inputs-.splunkcloud.com" }, { diff --git a/integrations/statsig-v2/assets/images/horizontal.svg b/integrations/statsig-v2/assets/images/horizontal.svg new file mode 100644 index 00000000..ec902590 --- /dev/null +++ b/integrations/statsig-v2/assets/images/horizontal.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/integrations/statsig-v2/assets/images/square.svg b/integrations/statsig-v2/assets/images/square.svg new file mode 100644 index 00000000..b1e9367f --- /dev/null +++ b/integrations/statsig-v2/assets/images/square.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrations/statsig-v2/manifest.json b/integrations/statsig-v2/manifest.json new file mode 100644 index 00000000..733d2801 --- /dev/null +++ b/integrations/statsig-v2/manifest.json @@ -0,0 +1,67 @@ +{ + "name": "StatSig Import", + "version": "1.0.0", + "overview": "Import resources from StatSig to LaunchDarkly.", + "description": "Import StatSig resources into LaunchDarkly. Currently supports segments (rule-based and id_list, up to 10,000 members per segment), created in every environment in your LaunchDarkly project. Additional resource types coming. For legacy flag imports, use the StatSig integration.", + "author": "LaunchDarkly", + "supportEmail": "support@launchdarkly.com", + "links": { + "site": "https://www.statsig.com", + "launchdarklyDocs": "https://launchdarkly.com/docs/home/import", + "privacyPolicy": "https://launchdarkly.com/policies/privacy/" + }, + "categories": ["automation", "import", "migration"], + "icons": { + "square": "assets/images/square.svg", + "horizontal": "assets/images/horizontal.svg" + }, + "allowIntegrationConfigurations": true, + "formVariables": [ + { + "key": "ldProject", + "name": "Project", + "type": "environmentSelector", + "description": "The LaunchDarkly project to import resources into. Imported segments are created in every environment in this project; the environment you pick here is only used to scope the picker — fan-out happens automatically.", + "isOptional": false + }, + { + "key": "statsigConsoleApiKey", + "name": "StatSig Console API Key", + "type": "string", + "isSecret": true, + "clearSecretsOnChange": true, + "description": "Console API key with read permissions on your StatSig project.", + "isOptional": false + }, + { + "key": "statsigTag", + "name": "StatSig Tag", + "type": "string", + "description": "Provide a tag to only import a subset of your StatSig resources based on the tag. Leave blank to import all.", + "isOptional": true, + "defaultValue": "" + }, + { + "key": "ldApiKey", + "name": "LaunchDarkly API Key", + "type": "string", + "isSecret": true, + "clearSecretsOnChange": true, + "description": "An API key with create permissions on the resources this integration imports (segments, etc.).", + "isOptional": false + }, + { + "key": "ldTag", + "name": "LaunchDarkly Tag", + "type": "string", + "description": "A tag to apply to all resources imported from StatSig to LaunchDarkly.", + "isOptional": true, + "defaultValue": "imported-from-statsig" + } + ], + "capabilities": { + "segmentImport": { + "includeErrorResponseBody": false + } + } +} diff --git a/integrations/statsig/assets/images/horizontal.svg b/integrations/statsig/assets/images/horizontal.svg new file mode 100644 index 00000000..ec902590 --- /dev/null +++ b/integrations/statsig/assets/images/horizontal.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/integrations/statsig/assets/images/square.svg b/integrations/statsig/assets/images/square.svg new file mode 100644 index 00000000..b1e9367f --- /dev/null +++ b/integrations/statsig/assets/images/square.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrations/statsig/manifest.json b/integrations/statsig/manifest.json new file mode 100644 index 00000000..f4f1834a --- /dev/null +++ b/integrations/statsig/manifest.json @@ -0,0 +1,76 @@ +{ + "name": "StatSig", + "version": "1.0.0", + "overview": "Import feature flags from StatSig to LaunchDarkly.", + "description": "Import StatSig Feature Gates as boolean flags or Dynamic Configs as JSON multi-variate flags into LaunchDarkly. Customize a tag to be assigned to each flag imported.", + "author": "LaunchDarkly", + "supportEmail": "support@launchdarkly.com", + "links": { + "site": "https://www.statsig.com", + "launchdarklyDocs": "https://launchdarkly.com/docs/home/flags/import", + "privacyPolicy": "https://launchdarkly.com/policies/privacy/" + }, + "categories": ["automation", "import", "migration"], + "icons": { + "square": "assets/images/square.svg", + "horizontal": "assets/images/horizontal.svg" + }, + "formVariables": [ + { + "key": "statsigConsoleApiKey", + "name": "StatSig Console API Key", + "type": "string", + "isSecret": true, + "clearSecretsOnChange": true, + "description": "Console API key with read permissions on your project.", + "isOptional": false + }, + { + "key": "statsigTag", + "name": "StatSig Tag", + "type": "string", + "description": "Provide a tag to only import a subset of your StatSig entities based on the tag. Leave blank to import all.", + "isOptional": true, + "defaultValue": "" + }, + { + "key": "statsigImportType", + "name": "Import Type", + "type": "enum", + "allowedValues": ["feature-gates", "dynamic-configs"], + "description": "Choose which StatSig entity type to import. Feature Gates become boolean flags. Dynamic Configs become JSON multi-variate flags. Defaults to feature-gates if left blank.", + "isOptional": true, + "defaultValue": "feature-gates" + }, + { + "key": "ldApiKey", + "name": "LaunchDarkly API Key", + "type": "string", + "isSecret": true, + "description": "An API key with create flag permissions on your LaunchDarkly account.", + "isOptional": false + }, + { + "key": "ldTag", + "name": "LaunchDarkly Tag", + "type": "string", + "description": "A tag to apply to all flags imported from StatSig to LaunchDarkly.", + "isOptional": true, + "defaultValue": "imported-from-statsig" + }, + { + "key": "ldMaintainer", + "name": "LaunchDarkly Maintainer", + "type": "string", + "description": "The member id of the LaunchDarkly user who will be the maintainer of the imported flags.", + "isOptional": true, + "isHidden": true, + "defaultValue": "" + } + ], + "capabilities": { + "flagImport": { + "includeErrorResponseBody": false + } + } +} diff --git a/integrations/terraform-cloud/manifest.json b/integrations/terraform-cloud/manifest.json index 30440bb3..69da3463 100644 --- a/integrations/terraform-cloud/manifest.json +++ b/integrations/terraform-cloud/manifest.json @@ -22,7 +22,8 @@ "type": "string", "description": "Enter your Terraform Enterprise host name in the format https://HOST_URL.", "isOptional": true, - "defaultValue": "https://app.terraform.io" + "defaultValue": "https://app.terraform.io", + "clearSecretsOnChange": true }, { "key": "token", diff --git a/integrations/unleash/manifest.json b/integrations/unleash/manifest.json index 296e2330..279a716a 100644 --- a/integrations/unleash/manifest.json +++ b/integrations/unleash/manifest.json @@ -21,6 +21,7 @@ "name": "Unleash Base URL", "type": "string", "isSecret": false, + "clearSecretsOnChange": true, "description": "The base URL used to connect to your Unleash instance", "isOptional": false, "placeholder": "https://unleash.mysite.com" diff --git a/integrations/vercel-native/README.md b/integrations/vercel-native/README.md new file mode 100644 index 00000000..99414010 --- /dev/null +++ b/integrations/vercel-native/README.md @@ -0,0 +1,146 @@ +# Vercel Native Integration + +## auditLogEventsHook endpoint + +The endpoint `method` and `url` are Handlebars templates that branch on the event context. Here's the logic in readable form. + +### Method + +Derived from `context.verbKind`: + +| verbKind | HTTP method | +| ---------------------- | ----------- | +| `createFlag` | POST | +| `cloneFlag` | POST | +| `updateName` | PATCH | +| `updateDescription` | PATCH | +| `updateGlobalArchived` | PATCH | +| `createProject` | PUT | +| `updateProjectName` | PATCH | +| `updateApiKey` | PATCH | +| `deleteFlag` | DELETE | +| `deleteProject` | DELETE | + +The `defaultPolicy` subscribes to exactly these verbKinds. Any unhandled verbKind would render an empty method string and fail at runtime. + +### URL + +``` +https://api.vercel.com/v1/installations/{installationId}/resources/{projectKey}[/experimentation/items[/{projectKey}_{flagKey}]] +``` + +- `installationId` — form variable +- `projectKey` — from event context: `context.project.key` for flag events, `context.key` for project events (where the resource itself is the project) +- `/experimentation/items` — appended for flag events only (`context.kind == "flag"`) +- `/{projectKey}_{flagKey}` — appended for all flag ops **except** create ops (`createFlag`, `cloneFlag`), which POST to the collection endpoint + +### Request bodies + +**createFlag / cloneFlag** — POSTs to the items collection: + +```json +{ + "items": [ + { + "id": "{projectKey}_{flagKey}", + "slug": "{flagKey}", + "origin": "{baseURL}/projects/{projectKey}/flags/{flagKey}", + "name": "{name}", + "description": "{description}", + "createdAt": 1234567890000, + "updatedAt": 1234567890000, + "category": "flag" + } + ] +} +``` + +**updateName / updateDescription / updateGlobalArchived** — PATCHes the specific item: + +```json +{ + "slug": "{flagKey}", + "origin": "{baseURL}/projects/{projectKey}/flags/{flagKey}", + "name": "{name}", + "description": "{description}", + "updatedAt": 1234567890000, + "category": "flag" +} +``` + +**deleteFlag**: + +```json +{ "_delete": true } +``` + +**createProject** — `PUT` to `…/installations/{installationId}/resources/{projectKey}`: + +```json +{ + "name": "{name || key}", + "productId": "adfsdfvsdf", + "status": "ready", + "secrets": [ + { + "name": "LAUNCHDARKLY_SDK_KEY", + "value": "{apiKey for production env}", + "environmentOverrides": { + "development": "{apiKey for development env}", + "preview": "{apiKey for preview env}", + "production": "{apiKey for production env}" + } + }, + { + "name": "LAUNCHDARKLY_CLIENT_SIDE_ID", + "value": "{clientId for production env}", + "environmentOverrides": { + "development": "{clientId for development env}", + "preview": "{clientId for preview env}", + "production": "{clientId for production env}" + } + } + ] +} +``` + +`environmentOverrides` keys come from `context.project.environments[*].key` directly — the LD project is expected to have envs named `development`, `preview`, and `production` to match Vercel's environment names. Each secret's `value` field is the production env's credential (Vercel requires it as a default for envs not in the overrides). Mobile keys are intentionally omitted. + +**updateApiKey** — `PATCH` to `…/installations/{installationId}/resources/{projectKey}`: + +```json +{ + "secrets": [ + { + "name": "LAUNCHDARKLY_SDK_KEY", + "value": "{apiKey for production env}", + "environmentOverrides": { + "development": "{apiKey for development env}", + "preview": "{apiKey for preview env}", + "production": "{apiKey for production env}" + } + }, + { + "name": "LAUNCHDARKLY_CLIENT_SIDE_ID", + "value": "{clientId for production env}", + "environmentOverrides": { + "development": "{clientId for development env}", + "preview": "{clientId for preview env}", + "production": "{clientId for production env}" + } + } + ] +} +``` + +Same secrets structure as `createProject`, but without `name`, `productId`, or `status` fields since this is an update to an existing resource. + +**updateProjectName**: + +```json +{ "name": "{name}" } +``` + +**deleteProject** — `DELETE` to `…/installations/{installationId}/resources/{projectKey}` per [Vercel Delete integration resource](https://vercel.com/docs/integrations/create-integration/marketplace-api/reference/vercel/delete-integration-resource). Request body is `{}`. + +> Note: `description` and `isArchived` are strings derived from the audit log event context (`payload.currentVersion`). Timestamps are numbers (milliseconds). diff --git a/integrations/vercel-native/manifest.json b/integrations/vercel-native/manifest.json index 9b7c6c6b..f6466c69 100644 --- a/integrations/vercel-native/manifest.json +++ b/integrations/vercel-native/manifest.json @@ -15,6 +15,7 @@ "square": "assets/square.svg", "horizontal": "assets/horizontal.svg" }, + "hideOnIntegrationsPage": true, "formVariables": [ { "key": "accessToken", @@ -23,10 +24,115 @@ "placeholder": "e.g. 8M7wS6hCpXVc-DoRnPPY_UCWPgy8aea4Wy6kCe5T", "type": "string", "isSecret": true + }, + { + "key": "installationId", + "name": "Installation ID", + "description": "Enter the installation ID generated from Vercel.", + "type": "string", + "isSecret": false } ], + "allowIntegrationConfigurations": true, "otherCapabilities": ["external"], "capabilities": { - "hideConfiguration": true + "hideConfiguration": true, + "featureStore": { + "formVariables": [ + { + "key": "accessToken", + "name": "Access Token", + "type": "string", + "description": "Vercel access token.", + "isSecret": true, + "isHidden": true + }, + { + "key": "installationId", + "name": "Installation ID", + "type": "string", + "description": "The Vercel installation ID.", + "isSecret": false, + "isHidden": true + }, + { + "key": "resourceId", + "name": "Vercel Resource ID", + "type": "string", + "description": "The Vercel resource ID for this project.", + "isSecret": false, + "isHidden": true + } + ], + "featureStoreRequest": { + "endpoint": { + "url": "https://api.vercel.com/v1/installations/{{installationId}}/resources/{{resourceId}}/experimentation/edge-config", + "method": "PUT", + "headers": [ + { + "name": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "User-Agent", + "value": "launchdarkly" + } + ] + }, + "payloadPrefix": "{\"data\": {\"LD-Env-{{envId}}\": ", + "payloadSuffix": "}}" + } + }, + "auditLogEventsHook": { + "includeErrorResponseBody": true, + "endpoint": { + "url": "https://api.vercel.com/v1/installations/{{installationId}}/resources/{{#equal context.kind \"flag\"}}{{context.project.key}}/experimentation/items{{#equal context.verbKind \"updateName\"}}/{{pathEncode context.project.key}}_{{pathEncode context.key}}{{/equal}}{{#equal context.verbKind \"updateDescription\"}}/{{pathEncode context.project.key}}_{{pathEncode context.key}}{{/equal}}{{#equal context.verbKind \"updateGlobalArchived\"}}/{{pathEncode context.project.key}}_{{pathEncode context.key}}{{/equal}}{{#equal context.verbKind \"deleteFlag\"}}/{{pathEncode context.project.key}}_{{pathEncode context.key}}{{/equal}}{{/equal}}{{#equal context.kind \"project\"}}{{context.key}}{{/equal}}{{#equal context.kind \"environment\"}}{{context.project.key}}{{/equal}}", + "method": "{{#equal context.verbKind \"createFlag\"}}POST{{/equal}}{{#equal context.verbKind \"cloneFlag\"}}POST{{/equal}}{{#equal context.verbKind \"updateName\"}}PATCH{{/equal}}{{#equal context.verbKind \"updateDescription\"}}PATCH{{/equal}}{{#equal context.verbKind \"updateGlobalArchived\"}}PATCH{{/equal}}{{#equal context.verbKind \"deleteFlag\"}}DELETE{{/equal}}{{#equal context.verbKind \"createProject\"}}PUT{{/equal}}{{#equal context.verbKind \"updateProjectName\"}}PATCH{{/equal}}{{#equal context.verbKind \"deleteProject\"}}DELETE{{/equal}}{{#equal context.verbKind \"updateApiKey\"}}PATCH{{/equal}}", + "headers": [ + { + "name": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "templates": { + "flag": "templates/flag.json.hbs", + "project": "templates/project.json.hbs", + "environment": "templates/environment.json.hbs", + "default": "templates/default.json.hbs" + }, + "defaultPolicy": [ + { + "effect": "allow", + "resources": ["proj/*:env/*:flag/*"], + "actions": [ + "createFlag", + "cloneFlag", + "updateName", + "updateDescription", + "updateGlobalArchived", + "deleteFlag" + ] + }, + { + "effect": "allow", + "resources": ["proj/*"], + "actions": ["createProject", "updateProjectName", "deleteProject"] + }, + { + "effect": "allow", + "resources": ["proj/*:env/*"], + "actions": ["updateApiKey"] + } + ] + } } } diff --git a/integrations/vercel-native/templates/default.json.hbs b/integrations/vercel-native/templates/default.json.hbs new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/integrations/vercel-native/templates/default.json.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/integrations/vercel-native/templates/environment.json.hbs b/integrations/vercel-native/templates/environment.json.hbs new file mode 100644 index 00000000..9173ac54 --- /dev/null +++ b/integrations/vercel-native/templates/environment.json.hbs @@ -0,0 +1 @@ +{{#if project.environments}}{"secrets":[{"name":"LAUNCHDARKLY_SDK_KEY","value":"{{#each project.environments}}{{#equal key "production"}}{{apiKey}}{{/equal}}{{/each}}","environmentOverrides":{ {{#each project.environments}}{{#unless @first}},{{/unless}}"{{key}}":"{{apiKey}}"{{/each}} }},{"name":"LAUNCHDARKLY_CLIENT_SIDE_ID","value":"{{#each project.environments}}{{#equal key "production"}}{{clientId}}{{/equal}}{{/each}}","environmentOverrides":{ {{#each project.environments}}{{#unless @first}},{{/unless}}"{{key}}":"{{clientId}}"{{/each}} }}]}{{else}}{}{{/if}} diff --git a/integrations/vercel-native/templates/flag.json.hbs b/integrations/vercel-native/templates/flag.json.hbs new file mode 100644 index 00000000..f6e71943 --- /dev/null +++ b/integrations/vercel-native/templates/flag.json.hbs @@ -0,0 +1 @@ +{{#equalWithElse verbKind "createFlag"}}{"items":[{"id":"{{project.key}}_{{key}}","slug":"{{key}}","origin":"{{baseURL}}/projects/{{project.key}}/flags/{{key}}","name":"{{name}}","description":"{{description}}","createdAt":{{timestamp.milliseconds}},"updatedAt":{{timestamp.milliseconds}},"category":"flag"}]}{{else}}{{#equalWithElse verbKind "cloneFlag"}}{"items":[{"id":"{{project.key}}_{{key}}","slug":"{{key}}","origin":"{{baseURL}}/projects/{{project.key}}/flags/{{key}}","name":"{{name}}","description":"{{description}}","createdAt":{{timestamp.milliseconds}},"updatedAt":{{timestamp.milliseconds}},"category":"flag"}]}{{else}}{{#equalWithElse verbKind "updateName"}}{"slug":"{{key}}","origin":"{{baseURL}}/projects/{{project.key}}/flags/{{key}}","name":"{{name}}","description":"{{description}}","updatedAt":{{timestamp.milliseconds}},"category":"flag"}{{else}}{{#equalWithElse verbKind "updateDescription"}}{"slug":"{{key}}","origin":"{{baseURL}}/projects/{{project.key}}/flags/{{key}}","name":"{{name}}","description":"{{description}}","updatedAt":{{timestamp.milliseconds}},"category":"flag"}{{else}}{{#equalWithElse verbKind "updateGlobalArchived"}}{"slug":"{{key}}","origin":"{{baseURL}}/projects/{{project.key}}/flags/{{key}}","name":"{{name}}","description":"{{description}}","updatedAt":{{timestamp.milliseconds}},"category":"flag"}{{else}}{{#equalWithElse verbKind "deleteFlag"}}{"_delete":true}{{else}}{}{{/equalWithElse}}{{/equalWithElse}}{{/equalWithElse}}{{/equalWithElse}}{{/equalWithElse}}{{/equalWithElse}} \ No newline at end of file diff --git a/integrations/vercel-native/templates/project.json.hbs b/integrations/vercel-native/templates/project.json.hbs new file mode 100644 index 00000000..9afb2637 --- /dev/null +++ b/integrations/vercel-native/templates/project.json.hbs @@ -0,0 +1 @@ +{{#equal verbKind "createProject"}}{"name":"{{#if name}}{{name}}{{else}}{{key}}{{/if}}","productId":"launchdarkly","status":"ready"{{#if project.environments}},"secrets":[{"name":"LAUNCHDARKLY_SDK_KEY","value":"{{#each project.environments}}{{#equal key "production"}}{{apiKey}}{{/equal}}{{/each}}","environmentOverrides":{ {{#each project.environments}}{{#unless @first}},{{/unless}}"{{key}}":"{{apiKey}}"{{/each}} }},{"name":"LAUNCHDARKLY_CLIENT_SIDE_ID","value":"{{#each project.environments}}{{#equal key "production"}}{{clientId}}{{/equal}}{{/each}}","environmentOverrides":{ {{#each project.environments}}{{#unless @first}},{{/unless}}"{{key}}":"{{clientId}}"{{/each}} }}]{{/if}} }{{else}}{{#equal verbKind "deleteProject"}}{}{{else}}{"name": "{{name}}"}{{/equal}}{{/equal}} diff --git a/manifest.schema.d.ts b/manifest.schema.d.ts index c29838c9..e8f85dad 100644 --- a/manifest.schema.d.ts +++ b/manifest.schema.d.ts @@ -284,9 +284,15 @@ export type AllowedValues = string[]; */ export type URL = string; /** - * HTTP method to use when LaunchDarkly makes the request to your endpoint + * HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE. */ -export type HTTPMethod = "POST" | "PUT" | "PATCH" | "GET" | "DELETE"; +export type HTTPMethod = ( + | ("POST" | "PUT" | "PATCH" | "GET" | "DELETE") + | { + [k: string]: unknown; + } +) & + string; /** * Name of the header */ @@ -761,6 +767,10 @@ export type StatSigTemplate = string; * Whether errors received while importing should be displayed in the error log in LaunchDarkly UI */ export type IncludeErrorResponseBody1 = boolean; +/** + * Whether errors received while importing should be displayed in the error log in the LaunchDarkly UI + */ +export type IncludeErrorResponseBody2 = boolean; /** * Template to use for measuredRolloutRegressionDetected events */ @@ -938,6 +948,7 @@ export interface Capabilities { syncedSegment?: SyncedSegment; bigSegmentStore?: BigSegmentStore; flagImport?: FlagImport; + segmentImport?: SegmentImport; eventsHook?: EventsHook; flagCleanup?: FlagCleanup; internalConfigurationURL?: InternalConfigurationURL; @@ -1279,6 +1290,13 @@ export interface FlagImportBodyTemplate { statsig?: StatSigTemplate; [k: string]: unknown; } +/** + * This capability enables importing segments to LaunchDarkly + */ +export interface SegmentImport { + includeErrorResponseBody: IncludeErrorResponseBody2; + [k: string]: unknown; +} /** * This capability will enable LaunchDarkly to send webhooks to your endpoint when particular events are observed. */ diff --git a/manifest.schema.json b/manifest.schema.json index 074cf2eb..e2dd56db 100644 --- a/manifest.schema.json +++ b/manifest.schema.json @@ -448,15 +448,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -807,6 +814,7 @@ "syncedSegment", "bigSegmentStore", "flagImport", + "segmentImport", "eventsHook", "flagCleanup", "internalConfigurationURL" @@ -852,15 +860,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -1183,15 +1198,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -1657,15 +1679,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -2172,15 +2201,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -2563,15 +2599,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -2765,15 +2808,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -2967,15 +3017,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -3169,15 +3226,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -3371,15 +3435,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -3723,15 +3794,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -4101,15 +4179,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -4266,15 +4351,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -4974,6 +5066,29 @@ } } }, + "segmentImport": { + "$id": "#/capability/segmentImport", + "title": "Segment Import", + "description": "This capability enables importing segments to LaunchDarkly", + "type": "object", + "propertyNames": { + "enum": [ + "includeErrorResponseBody" + ] + }, + "required": [ + "includeErrorResponseBody" + ], + "properties": { + "includeErrorResponseBody": { + "$id": "#/properties/include-error-response-body", + "title": "Include error response body", + "type": "boolean", + "description": "Whether errors received while importing should be displayed in the error log in the LaunchDarkly UI", + "default": true + } + } + }, "eventsHook": { "$id": "#/properties/capability/events-hook", "title": "Events hook", @@ -5011,15 +5126,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", @@ -5341,15 +5463,22 @@ "method": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": [ - "POST", - "PUT", - "PATCH", - "GET", - "DELETE" - ], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { + "enum": [ + "POST", + "PUT", + "PATCH", + "GET", + "DELETE" + ] + }, + { + "pattern": "\\{\\{" + } + ] }, "headers": { "$id": "#/definitions/endpoint/headers", diff --git a/sample-context/project/create.json b/sample-context/project/create.json index 4df031a1..da6014e8 100644 --- a/sample-context/project/create.json +++ b/sample-context/project/create.json @@ -45,7 +45,25 @@ "name": "", "key": "", "tags": null - } + }, + "environments": [ + { + "key": "production", + "name": "Production", + "tags": [], + "apiKey": "sdk-prd-aaaaaaaa", + "mobileKey": "mob-prd-bbbbbbbb", + "clientId": "cccccccccccccccc" + }, + { + "key": "test", + "name": "Test", + "tags": [], + "apiKey": "sdk-tst-dddddddd", + "mobileKey": "mob-tst-eeeeeeee", + "clientId": "ffffffffffffffff" + } + ] }, "key": "a-new-project", "tags": [], diff --git a/schemas/base.json b/schemas/base.json index 42cab357..53eeb7ba 100644 --- a/schemas/base.json +++ b/schemas/base.json @@ -280,6 +280,7 @@ "syncedSegment", "bigSegmentStore", "flagImport", + "segmentImport", "eventsHook", "flagCleanup", "internalConfigurationURL" @@ -325,6 +326,9 @@ "flagImport": { "$ref": "schemas/capabilities/flagImport.json#/flagImport" }, + "segmentImport": { + "$ref": "schemas/capabilities/segmentImport.json#/segmentImport" + }, "eventsHook": { "$ref": "schemas/capabilities/eventsHook.json#/eventsHook" }, diff --git a/schemas/capabilities/segmentImport.json b/schemas/capabilities/segmentImport.json new file mode 100644 index 00000000..21004885 --- /dev/null +++ b/schemas/capabilities/segmentImport.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "segmentImport": { + "$id": "#/capability/segmentImport", + "title": "Segment Import", + "description": "This capability enables importing segments to LaunchDarkly", + "type": "object", + "propertyNames": { + "enum": ["includeErrorResponseBody"] + }, + "required": ["includeErrorResponseBody"], + "properties": { + "includeErrorResponseBody": { + "$id": "#/properties/include-error-response-body", + "title": "Include error response body", + "type": "boolean", + "description": "Whether errors received while importing should be displayed in the error log in the LaunchDarkly UI", + "default": true + } + } + } +} diff --git a/schemas/definitions.json b/schemas/definitions.json index 4f744612..b8ef2815 100644 --- a/schemas/definitions.json +++ b/schemas/definitions.json @@ -17,9 +17,12 @@ "endpointMethod": { "$id": "#/definitions/endpoint/method", "title": "HTTP method", - "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint", - "enum": ["POST", "PUT", "PATCH", "GET", "DELETE"], - "type": "string" + "description": "HTTP method to use when LaunchDarkly makes the request to your endpoint. You can use {{template markup}} to inject a formVariable for dynamic method resolution. Resolved value must be one of POST, PUT, PATCH, GET, or DELETE.", + "type": "string", + "anyOf": [ + { "enum": ["POST", "PUT", "PATCH", "GET", "DELETE"] }, + { "pattern": "\\{\\{" } + ] }, "endpointHeaders": { "$id": "#/definitions/endpoint/headers",