Datafile

Fetch the datafile: every active test, flag and audience for one space and environment.

The datafile contains the complete configuration of an environment for one space. It contains all flags and tests that are active and links the audiences required for those entries.

Url

https://improve.obelism.studio/config/:spaceId/:environment/:status?

  • Method: GET
  • spaceId - Unique identifier of the space; space_XXX. Copy it from Implementation on the Settings page. An organization id (org_XXX) works too, and loads the organization's first space: that is what the SDK sends.
  • environment - environment to load; 'develop' | 'staging' | 'production'
  • status - (optional) which test state to return; 'draft' | 'active' | 'finished' | 'archived'. When omitted it defaults to active, so /config/:spaceId/:environment returns the same as /config/:spaceId/:environment/active.

The status segment lets you preview tests in another state before they go live: for example, request draft to validate a test you're still setting up. Production traffic should keep loading active (the default).

A space or organization id that doesn't exist, or belongs to an Organization that was deleted, gets 404 with the code unknown_organization. The SDK doesn't retry it. Path parameters that aren't valid at all (an id without the space_ or org_ prefix, an unknown environment or status) still get an empty datafile, {}, so the SDK falls back to its defaults.

Authentication

How a request is checked depends on whether it sends an Origin (see Security):

  • Browser (client) requests send an Origin, which must be in the environment's Allowed origins (configured in the console; an entry ending in :*, like develop's default http://localhost:*, matches any port). This is enforced via CORS: an origin that isn't allow-listed receives a 403.
  • Server requests don't send an Origin and are served without a token, rate limited per IP (see below). Send the environment's API token in a token header to lift that limit (the edge limit still applies). A token that isn't valid isn't refused: the request is served and rate limited like one without a token.

Allowed origins decide which sites can load the datafile in a browser; they don't make it secret. Anyone who knows the Organization id can fetch it without an Origin, so don't put anything in a test or flag that visitors shouldn't see.

Rate limiting

The browser (Origin) path is rate-limited per IP (currently 600 requests/minute); exceeding it returns 429. Browser responses carry RateLimit-Policy: "browser";q=600;w=60, and a 429 adds RateLimit: "browser";r=0;t=60 and Retry-After: 60 (see Versioning). Requests with a valid token are exempt, so server-side rendering behind a shared egress IP is only held to the edge limit below. A token that isn't one of the environment's API tokens is rate limited like a request without one.

Every datafile request, with or without a token and cached or not, is also limited at Cloudflare's edge to 100 per 10 seconds per IP. Past it, the address gets a 429 rate_limited for 10 seconds. That response comes from the edge, not the API: it is application/json (the same problem fields) without RateLimit or Retry-After headers, so wait 10 seconds before retrying.

Responses are also served with a short CDN cache (30 seconds, then served stale for up to 60 more while it refreshes in the background, with Vary: Origin), which absorbs repeated identical requests before they reach the origin. Browsers don't cache it (Cache-Control: public, max-age=0). Saving a test, flag or audience clears the cached datafile, so the change usually shows up on the next request; in the rare case that fails, it takes up to about 90 seconds. Changes to API tokens and allowed origins take up to about two and a half minutes.

Caching

Loading the datafile is one of the metrics on Improve that could increase costs. If you'd run a test on the server and want to post analytics from the client, this could mean two requests to the datafile per visitor. To make sure you don't rapidly chew through your usage plan you can cache this file in a few ways;

Add a caching layer

Instead of requesting the file from Improve for every visitor you could place it in your own key/value storage (like Redis). Here you can configure a time-to-live, ttl for short, and load it once every 5 minutes or per hour. This can be a great solution if you're not making changes on the fly. However, keep in mind that disabling all traffic from a flag back to the original will have the caching time as a delay.

Statically generate

If you hardly make any changes in your config but you have one AB test running for a few weeks, or you're a solo developer that can easily deploy when you make a change, it could be an idea to grab the file on deploy and host it yourself in your project. A way of doing this is with pre-deploy scripts. For example, if you host using GitHub Actions, you could add a task to grab it then. For hosting providers like Vercel and Netlify you utilize a prebuild script to download the latest datafile at that point.

Type

Audiences match against parameters derived from the visitor. Most (pointer, device, browser, os) come from the user agent and resolve anywhere. country (ISO 3166-1 alpha-2, e.g. NL) is the exception: the browser can't determine it from the user agent, so it only resolves during server-side / middleware bucketing: the Next.js middleware supplies it automatically from the edge, or you pass it into the server SDK's getTestValue / getFlagValue geo argument. A country audience never matches client-side.

type AudienceParamKey = 'country' | 'pointer' | 'device' | 'browser' | 'os'

export type Configuration = {
	name: string
	version: number
	flags: {
		[flagSlug in string]: {
			id: string
			name: string
			audience: string
			options: {
				name: string
				slug: string
				value: string | undefined
				split: number
			}[]
		}
	}
	tests: {
		[testSlug in string]: {
			id: string
			name: string
			defaultValue: string
			audience: string
			allocation: number
			options: {
				name: string
				slug: string
				value: string | undefined
				split: number
			}[]
			events: Events
		}
	}
	audience: {
		[audienceSlug in string]: {
			[Key in AudienceParamKey]: string
		}
	}
}

Example

{
  "name": "obelism",
  "version": 1,
  "flags": {
    "feature-test": {
      "id": "feat_VGEFKIJEXBLBRB5KUCZJSTNMD7",
      "name": "Feature test",
      "audience": "cooler-users",
      "options": [
        {
          "name": "Original",
          "slug": "original",
          "value": "a",
          "split": 80
        },
        {
          "name": "Alteration",
          "slug": "alteration",
          "value": "b",
          "split": 20
        }
      ]
    }
  },
  "tests": {
    "duo-test": {
      "id": "test_AM1TCFPYQW08EQFNWHL5ZSP8VB",
      "name": "Duo test",
      "defaultValue": "control",
      "allocation": 100,
      "audience": "",
      "options": [
        {
          "name": "Control",
          "slug": "control",
          "value": "a",
          "split": 50
        },
        {
          "name": "Variation",
          "slug": "variation",
          "value": "b",
          "split": 50
        }
      ],
      "events": {
        "start": "page_view",
        "metrics": [],
        "conversion": "sign_up"
      }
    }
  },
  "audience": {
    "cooler-users": {
      "device": "tablet",
      "os": "ios"
    },
    "dutch-visitors": {
      "country": "NL"
    }
  }
}

On this page