@crvouga/mockingbird-service-posthog 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -251,8 +251,8 @@ var createControlPlane = (context) => {
251
251
  preset,
252
252
  rules: context.applyPreset(preset, namespace, overrides)
253
253
  });
254
- } catch (error2) {
255
- return adminError(404, error2 instanceof Error ? error2.message : String(error2));
254
+ } catch (error3) {
255
+ return adminError(404, error3 instanceof Error ? error3.message : String(error3));
256
256
  }
257
257
  }
258
258
  if (!isRecord(body) || typeof body.status !== "number" && typeof body.delayMs !== "number" && typeof body.latencyMs !== "number" && body.drop !== true && typeof body.effect !== "string") {
@@ -307,8 +307,8 @@ var createControlPlane = (context) => {
307
307
  const branch = isRecord(body) && typeof body.branch === "string" ? body.branch : "main";
308
308
  try {
309
309
  return json(201, context.timeTravel.checkpoint(namespace, branch));
310
- } catch (error2) {
311
- return adminError(409, error2 instanceof Error ? error2.message : String(error2));
310
+ } catch (error3) {
311
+ return adminError(409, error3 instanceof Error ? error3.message : String(error3));
312
312
  }
313
313
  },
314
314
  "POST /branches/:name": ({ params, body, namespace }) => {
@@ -318,8 +318,8 @@ var createControlPlane = (context) => {
318
318
  namespace,
319
319
  ...at !== void 0 ? { at } : {}
320
320
  }));
321
- } catch (error2) {
322
- return adminError(409, error2 instanceof Error ? error2.message : String(error2));
321
+ } catch (error3) {
322
+ return adminError(409, error3 instanceof Error ? error3.message : String(error3));
323
323
  }
324
324
  },
325
325
  "POST /branches/:name/checkout": ({ params, body, namespace }) => {
@@ -332,8 +332,8 @@ var createControlPlane = (context) => {
332
332
  branch: params.name
333
333
  });
334
334
  return json(200, { status: "ok", branch: params.name, checkpoint: body.checkpoint });
335
- } catch (error2) {
336
- return adminError(409, error2 instanceof Error ? error2.message : String(error2));
335
+ } catch (error3) {
336
+ return adminError(409, error3 instanceof Error ? error3.message : String(error3));
337
337
  }
338
338
  },
339
339
  "GET /requests": ({ url, namespace }) => {
@@ -810,12 +810,12 @@ var decodeBody = (contentType, bytes) => {
810
810
  const text = utf8.decode(bytes);
811
811
  try {
812
812
  return { kind: "json", value: JSON.parse(text) };
813
- } catch (error2) {
813
+ } catch (error3) {
814
814
  return {
815
815
  kind: "invalid",
816
816
  mediaType,
817
817
  text,
818
- error: error2 instanceof Error ? error2.message : String(error2)
818
+ error: error3 instanceof Error ? error3.message : String(error3)
819
819
  };
820
820
  }
821
821
  }
@@ -827,7 +827,40 @@ var decodeBody = (contentType, bytes) => {
827
827
  return { kind: "bytes", value: bytes };
828
828
  };
829
829
  var readBody = async (message) => {
830
- const bytes = new Uint8Array(await message.arrayBuffer());
830
+ if (!(message instanceof Request) || !message.body) {
831
+ if (message instanceof Request)
832
+ message.signal.throwIfAborted();
833
+ return decodeBody(message.headers.get("content-type"), new Uint8Array(await message.arrayBuffer()));
834
+ }
835
+ message.signal.throwIfAborted();
836
+ const reader = message.body.getReader();
837
+ const cancel = () => {
838
+ void reader.cancel(message.signal.reason).catch(() => {
839
+ });
840
+ };
841
+ message.signal.addEventListener("abort", cancel, { once: true });
842
+ const chunks = [];
843
+ let size = 0;
844
+ try {
845
+ for (; ; ) {
846
+ message.signal.throwIfAborted();
847
+ const { done, value } = await reader.read();
848
+ message.signal.throwIfAborted();
849
+ if (done)
850
+ break;
851
+ chunks.push(value);
852
+ size += value.byteLength;
853
+ }
854
+ } finally {
855
+ message.signal.removeEventListener("abort", cancel);
856
+ reader.releaseLock();
857
+ }
858
+ const bytes = new Uint8Array(size);
859
+ let offset = 0;
860
+ for (const chunk of chunks) {
861
+ bytes.set(chunk, offset);
862
+ offset += chunk.byteLength;
863
+ }
831
864
  return decodeBody(message.headers.get("content-type"), bytes);
832
865
  };
833
866
 
@@ -1499,7 +1532,42 @@ var verifyOperations = (document2, handlers) => {
1499
1532
  }
1500
1533
  return problems;
1501
1534
  };
1502
- var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
1535
+ var escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1536
+ var serviceRoute = (template) => {
1537
+ const names = [];
1538
+ let pattern = "", end = 0;
1539
+ for (const match of template.matchAll(/\{([^}]+)\}/g)) {
1540
+ pattern += `${escapePattern(template.slice(end, match.index))}([^/]+?)`;
1541
+ const name = match[1];
1542
+ if (!name)
1543
+ throw new Error(`Invalid OpenAPI route template: ${template}`);
1544
+ names.push(name);
1545
+ end = match.index + match[0].length;
1546
+ }
1547
+ pattern += escapePattern(template.slice(end));
1548
+ const matcher = new RegExp(`^${pattern}$`);
1549
+ const path = template.split("/").map((segment, index) => {
1550
+ if (!segment.includes("{"))
1551
+ return segment;
1552
+ const literal = /^\{([^}]+)\}$/.exec(segment);
1553
+ if (literal)
1554
+ return `:${literal[1]}`;
1555
+ let expression = "", offset = 0;
1556
+ for (const match of segment.matchAll(/\{[^}]+\}/g)) {
1557
+ expression += `${escapePattern(segment.slice(offset, match.index))}[^/]+?`;
1558
+ offset = match.index + match[0].length;
1559
+ }
1560
+ expression += escapePattern(segment.slice(offset));
1561
+ return `:mockingbirdSegment${index}{${expression}}`;
1562
+ }).join("/");
1563
+ return {
1564
+ path,
1565
+ params: (pathname) => {
1566
+ const values = matcher.exec(pathname);
1567
+ return Object.fromEntries(names.map((name, index) => [name, decodeURIComponent(values?.[index + 1] ?? "")]));
1568
+ }
1569
+ };
1570
+ };
1503
1571
  var routeOrder = (a, b) => {
1504
1572
  const sa = a.path.split("/");
1505
1573
  const sb = b.path.split("/");
@@ -1529,11 +1597,12 @@ var createService = (options) => {
1529
1597
  const now = options.now ?? (() => Date.now());
1530
1598
  const app = new Hono();
1531
1599
  app.notFound((c) => options.notFound(c.req.raw));
1532
- app.onError((error2, c) => options.onError(error2, c.req.raw));
1600
+ app.onError((error3, c) => options.onError(error3, c.req.raw));
1533
1601
  const operations = [...listOperations(options.document)].sort(routeOrder);
1534
1602
  for (const operation of operations) {
1535
1603
  const metadata = operationMetadata(operation.operation);
1536
1604
  const handler = options.handlers[operation.operationId];
1605
+ const registeredRoute = serviceRoute(operation.path);
1537
1606
  const route = async (c) => {
1538
1607
  const request = c.req.raw;
1539
1608
  if (!metadata.supported || !handler) {
@@ -1543,7 +1612,7 @@ var createService = (options) => {
1543
1612
  const context = {
1544
1613
  request,
1545
1614
  url,
1546
- params: c.req.param(),
1615
+ params: registeredRoute.params(url.pathname),
1547
1616
  query: queryOf(url),
1548
1617
  body: await readBody(request),
1549
1618
  sqlite: options.sqlite,
@@ -1555,7 +1624,7 @@ var createService = (options) => {
1555
1624
  const short = await options.before?.(context);
1556
1625
  return withIssues(request, short ?? await handler(context));
1557
1626
  };
1558
- app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
1627
+ app.on(operation.method.toUpperCase(), registeredRoute.path, route);
1559
1628
  }
1560
1629
  return {
1561
1630
  app,
@@ -1936,8 +2005,8 @@ var stateAdminRoutes = (routes) => {
1936
2005
  const open = (namespace) => {
1937
2006
  try {
1938
2007
  return { ok: true, value: routes.open(namespace) };
1939
- } catch (error2) {
1940
- return fail(400, error2 instanceof Error ? error2.message : String(error2));
2008
+ } catch (error3) {
2009
+ return fail(400, error3 instanceof Error ? error3.message : String(error3));
1941
2010
  }
1942
2011
  };
1943
2012
  const wrote = (namespace) => {
@@ -2024,7 +2093,7 @@ var stateAdminRoutes = (routes) => {
2024
2093
  };
2025
2094
 
2026
2095
  // ../core/dist/version.js
2027
- var PACKAGE_VERSION = true ? "2.1.0" : UNRELEASED_VERSION;
2096
+ var PACKAGE_VERSION = true ? "2.2.0" : UNRELEASED_VERSION;
2028
2097
 
2029
2098
  // ../core/dist/signing.js
2030
2099
  var encoder = new TextEncoder();
@@ -2653,9 +2722,9 @@ var createRuntime = (options) => {
2653
2722
  } else {
2654
2723
  storage = ensureBranch(namespace, selectedBranch, at);
2655
2724
  }
2656
- } catch (error2) {
2725
+ } catch (error3) {
2657
2726
  log(409);
2658
- return stamp(adminFail(409, error2 instanceof Error ? error2.message : String(error2)));
2727
+ return stamp(adminFail(409, error3 instanceof Error ? error3.message : String(error3)));
2659
2728
  }
2660
2729
  const hits = await faults.take({
2661
2730
  operationId,
@@ -3070,7 +3139,7 @@ var restView = (flag) => {
3070
3139
  variant: typeof flag.default === "string" ? flag.default : null
3071
3140
  });
3072
3141
  }
3073
- const variants = [
3142
+ const variants2 = [
3074
3143
  ...new Set(
3075
3144
  [flag.default, ...flag.overrides.map((o) => o.value)].filter(
3076
3145
  (v) => typeof v === "string"
@@ -3090,12 +3159,12 @@ var restView = (flag) => {
3090
3159
  created_at: flag.created_at,
3091
3160
  updated_at: flag.updated_at,
3092
3161
  version: flag.version,
3093
- filters: {
3162
+ filters: flag.filters ?? {
3094
3163
  groups,
3095
- multivariate: variants.length > 0 ? {
3096
- variants: variants.map((key, index) => ({
3164
+ multivariate: variants2.length > 0 ? {
3165
+ variants: variants2.map((key, index) => ({
3097
3166
  key,
3098
- rollout_percentage: Math.floor(100 / variants.length) + (index < 100 % variants.length ? 1 : 0)
3167
+ rollout_percentage: Math.floor(100 / variants2.length) + (index < 100 % variants2.length ? 1 : 0)
3099
3168
  }))
3100
3169
  } : null,
3101
3170
  payloads
@@ -3107,8 +3176,8 @@ var fromFilters = (filters) => {
3107
3176
  const record = isRecord6(filters) ? filters : {};
3108
3177
  const groups = Array.isArray(record.groups) ? record.groups.filter(isRecord6) : [];
3109
3178
  const multivariate = isRecord6(record.multivariate) ? record.multivariate : void 0;
3110
- const variants = Array.isArray(multivariate?.variants) ? multivariate.variants.filter(isRecord6) : [];
3111
- const topVariant = [...variants].sort((a, b) => Number(b.rollout_percentage ?? 0) - Number(a.rollout_percentage ?? 0)).map((v) => v.key).find((key) => typeof key === "string" && key.length > 0);
3179
+ const variants2 = Array.isArray(multivariate?.variants) ? multivariate.variants.filter(isRecord6) : [];
3180
+ const topVariant = [...variants2].sort((a, b) => Number(b.rollout_percentage ?? 0) - Number(a.rollout_percentage ?? 0)).map((v) => v.key).find((key) => typeof key === "string" && key.length > 0);
3112
3181
  const on = (group) => {
3113
3182
  const rollout = group.rollout_percentage ?? 100;
3114
3183
  if (typeof rollout === "number" && rollout <= 0) return false;
@@ -3141,10 +3210,188 @@ var fromFilters = (filters) => {
3141
3210
  return { default: fallback, payload, overrides };
3142
3211
  };
3143
3212
 
3213
+ // src/experiments.ts
3214
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3215
+ var error = (status, detail) => jsonRes(status, {
3216
+ type: status === 404 ? "invalid_request" : "validation_error",
3217
+ code: status === 404 ? "not_found" : "invalid_input",
3218
+ detail,
3219
+ attr: null
3220
+ });
3221
+ var variants = [
3222
+ { key: "control", name: "Control Group", rollout_percentage: 50 },
3223
+ { key: "test", name: "Test Variant", rollout_percentage: 50 }
3224
+ ];
3225
+ var legacyVariants = (parameters) => Array.isArray(parameters.feature_flag_variants) ? parameters.feature_flag_variants.filter(isRecord7).map((variant) => ({
3226
+ ...variant,
3227
+ rollout_percentage: variant.rollout_percentage ?? variant.split_percent
3228
+ })) : void 0;
3229
+ var experimentOperations = (state, now, adminPrefix) => {
3230
+ const view = (record) => {
3231
+ const { projectId: _project, ...body } = record;
3232
+ const flag = state.flags.get(record.feature_flag_key);
3233
+ const wire = flag ? restView(flag) : null;
3234
+ const multivariate = wire && isRecord7(wire.filters.multivariate) ? wire.filters.multivariate : void 0;
3235
+ const liveVariants = Array.isArray(multivariate?.variants) ? multivariate.variants.filter(isRecord7).map((v) => ({ ...v, split_percent: v.rollout_percentage })) : [];
3236
+ return {
3237
+ ...body,
3238
+ parameters: { ...record.parameters, feature_flag_variants: liveVariants },
3239
+ feature_flag: wire
3240
+ };
3241
+ };
3242
+ const find = (context) => {
3243
+ const record = state.experiments.get(context.params.experimentId ?? "");
3244
+ return record?.projectId === context.params.projectId ? record : void 0;
3245
+ };
3246
+ const validate = (body, current) => {
3247
+ if (!isRecord7(body)) return "Expected a JSON object.";
3248
+ if ((!current || body.name !== void 0) && (typeof body.name !== "string" || !body.name.trim()))
3249
+ return "name is required.";
3250
+ if (!current && (typeof body.feature_flag_key !== "string" || !/^[A-Za-z0-9_-]+$/.test(body.feature_flag_key)))
3251
+ return "feature_flag_key is required.";
3252
+ if (current && body.feature_flag_key !== void 0 && body.feature_flag_key !== current.feature_flag_key)
3253
+ return "The linked feature flag cannot be changed.";
3254
+ if (body.parameters !== void 0 && !isRecord7(body.parameters))
3255
+ return "parameters must be an object.";
3256
+ if (body.metrics !== void 0 && (!Array.isArray(body.metrics) || !body.metrics.every(isRecord7)))
3257
+ return "metrics must be an array of objects.";
3258
+ if (isRecord7(body.parameters)) {
3259
+ const roll = body.parameters.rollout_percentage;
3260
+ if (roll !== void 0 && (typeof roll !== "number" || roll < 0 || roll > 100))
3261
+ return "Invalid rollout_percentage.";
3262
+ const variants2 = legacyVariants(body.parameters);
3263
+ if (body.parameters.feature_flag_variants !== void 0 && (!Array.isArray(body.parameters.feature_flag_variants) || !variants2 || variants2.length < 2 || variants2.some(
3264
+ (v) => typeof v.key !== "string" || !v.key || typeof v.rollout_percentage !== "number" || v.rollout_percentage < 0 || v.rollout_percentage > 100
3265
+ ) || variants2.reduce((sum, v) => sum + Number(v.rollout_percentage), 0) !== 100))
3266
+ return "Variant weights must sum to 100.";
3267
+ }
3268
+ if (body.archived !== void 0 && typeof body.archived !== "boolean")
3269
+ return "archived must be boolean.";
3270
+ for (const field of ["start_date", "end_date"])
3271
+ if (body[field] !== void 0 && body[field] !== null && (typeof body[field] !== "string" || !Number.isFinite(Date.parse(body[field]))))
3272
+ return `${field} must be a date or null.`;
3273
+ const start = body.start_date === void 0 ? current?.start_date : body.start_date;
3274
+ const end = body.end_date === void 0 ? current?.end_date : body.end_date;
3275
+ if (typeof start === "string" && typeof end === "string" && Date.parse(start) >= Date.parse(end))
3276
+ return "start_date must precede end_date.";
3277
+ return void 0;
3278
+ };
3279
+ return {
3280
+ ListExperiments: (context) => {
3281
+ const limit = Number(context.query.limit ?? 100);
3282
+ const offset = Number(context.query.offset ?? 0);
3283
+ if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(offset) || offset < 0)
3284
+ return error(400, "Invalid pagination.");
3285
+ const rows = state.experiments.list({ order: "newest", where: (row) => row.projectId === context.params.projectId }).map((row) => row.value);
3286
+ const page = (at) => {
3287
+ const url = new URL(context.url);
3288
+ const namespace = context.request.headers.get("x-mockingbird-namespace");
3289
+ url.pathname = `${namespace ? `${adminPrefix}/ns/${encodeURIComponent(namespace)}` : ""}${context.url.pathname.replace(/\/$/, "")}/`;
3290
+ url.searchParams.set("limit", String(limit));
3291
+ url.searchParams.set("offset", String(at));
3292
+ return url.href;
3293
+ };
3294
+ return jsonRes(200, {
3295
+ count: rows.length,
3296
+ next: offset + limit < rows.length ? page(offset + limit) : null,
3297
+ previous: offset > 0 ? page(Math.max(0, offset - limit)) : null,
3298
+ results: rows.slice(offset, offset + limit).map(view)
3299
+ });
3300
+ },
3301
+ CreateExperiment: (context) => {
3302
+ const body = context.body.kind === "json" ? context.body.value : void 0;
3303
+ const issue = validate(body);
3304
+ if (issue || !isRecord7(body)) return error(400, issue ?? "Invalid body.");
3305
+ const parameters = isRecord7(body.parameters) ? body.parameters : {};
3306
+ const key = String(body.feature_flag_key);
3307
+ const existing = state.flags.get(key);
3308
+ if (existing?.deleted) return error(400, "The feature flag has been deleted.");
3309
+ if (!existing) {
3310
+ const flagFilters = {
3311
+ aggregation_group_type_index: null,
3312
+ groups: [
3313
+ { properties: [], rollout_percentage: Number(parameters.rollout_percentage ?? 100) }
3314
+ ],
3315
+ multivariate: { variants: legacyVariants(parameters) ?? variants },
3316
+ payloads: isRecord7(parameters.feature_flag_payloads) ? parameters.feature_flag_payloads : {}
3317
+ };
3318
+ state.putFlag(key, {
3319
+ ...fromFilters(flagFilters),
3320
+ filters: flagFilters,
3321
+ name: `Feature Flag for Experiment ${body.name}`,
3322
+ active: typeof body.start_date === "string"
3323
+ });
3324
+ }
3325
+ const id = state.experiments.list().reduce((max, row) => Math.max(max, row.value.id), 0) + 1;
3326
+ const timestamp = new Date(now()).toISOString();
3327
+ const record = {
3328
+ id,
3329
+ projectId: context.params.projectId ?? "",
3330
+ name: String(body.name),
3331
+ feature_flag_key: key,
3332
+ parameters,
3333
+ metrics: Array.isArray(body.metrics) ? body.metrics : [],
3334
+ start_date: typeof body.start_date === "string" ? body.start_date : null,
3335
+ end_date: typeof body.end_date === "string" ? body.end_date : null,
3336
+ archived: body.archived === true,
3337
+ created_at: timestamp,
3338
+ updated_at: timestamp
3339
+ };
3340
+ state.experiments.insert(String(id), record);
3341
+ return jsonRes(201, view(record));
3342
+ },
3343
+ GetExperiment: (context) => {
3344
+ const record = find(context);
3345
+ return record ? jsonRes(200, view(record)) : error(404, "Not found.");
3346
+ },
3347
+ UpdateExperiment: (context) => {
3348
+ const record = find(context);
3349
+ if (!record) return error(404, "Not found.");
3350
+ const body = context.body.kind === "json" ? context.body.value : void 0;
3351
+ const issue = validate(body, record);
3352
+ if (issue || !isRecord7(body)) return error(400, issue ?? "Invalid body.");
3353
+ const updated = { ...record, updated_at: new Date(now()).toISOString() };
3354
+ for (const field of [
3355
+ "name",
3356
+ "parameters",
3357
+ "metrics",
3358
+ "start_date",
3359
+ "end_date",
3360
+ "archived"
3361
+ ])
3362
+ if (body[field] !== void 0) Object.assign(updated, { [field]: body[field] });
3363
+ const flag = state.flags.get(record.feature_flag_key);
3364
+ const parameters = isRecord7(body.parameters) ? body.parameters : void 0;
3365
+ const configChanged = parameters && ["feature_flag_variants", "rollout_percentage", "feature_flag_payloads"].some(
3366
+ (key) => key in parameters
3367
+ );
3368
+ if (flag && configChanged) {
3369
+ if (record.start_date)
3370
+ return error(400, "Edit a running experiment's flag through the feature flag API.");
3371
+ const filters = { ...restView(flag).filters };
3372
+ if (parameters?.feature_flag_variants !== void 0)
3373
+ filters.multivariate = { variants: legacyVariants(parameters) ?? [] };
3374
+ if (parameters?.rollout_percentage !== void 0)
3375
+ filters.groups = [{ properties: [], rollout_percentage: parameters.rollout_percentage }];
3376
+ if (isRecord7(parameters?.feature_flag_payloads))
3377
+ filters.payloads = parameters.feature_flag_payloads;
3378
+ state.patchFlag(flag.key, {
3379
+ ...fromFilters(filters),
3380
+ filters,
3381
+ ...!record.start_date && updated.start_date ? { active: true } : {}
3382
+ });
3383
+ } else if (flag && !record.start_date && updated.start_date && !flag.active)
3384
+ state.patchFlag(flag.key, { active: true });
3385
+ state.experiments.update(String(record.id), updated);
3386
+ return jsonRes(200, view(updated));
3387
+ }
3388
+ };
3389
+ };
3390
+
3144
3391
  // src/generated/openapi.ts
3145
- var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"PostHog feature flags, capture and management API (Mockingbird subset)","description":"Stateful mock subset of PostHog: remote flag evaluation (\`/flags\` v2 and the legacy\\n\`/decide\` shape), remote config, event capture (\`/batch/\`, \`/e/\`, \`/i/v0/e/\`), session\\nrecording intake, the posthog-js asset and survey endpoints, and the slice of the\\nmanagement API our tooling and crons call (feature-flag list/create/patch, HogQL query).\\nHand-authored from the wire shapes of posthog-node 5.52.2 / @posthog/core 1.54.0,\\nposthog-js 1.433.2 and our own raw fetches.\\n\\nPaths are written without the trailing slash PostHog uses; the mock answers both\\n(\`/flags/?v=2\` from the SDKs and \`/flags?v=2\` from the EMR frontend's raw fetch).\\nCapture and flag bodies may arrive gzip-compressed (\`Content-Encoding: gzip\`, or raw gzip\\nbytes from posthog-js), base64 (\`data=\` form bodies), or as plain JSON; the contract\\ndeclares the decoded JSON.\\n","version":"1","x-mockingbird-upstream":{"note":"Hand-authored from @posthog/core posthog-core-stateless (getFlags, sendBatch, getRemoteConfig, getSurveysStateless), posthog-js request.ts / posthog-featureflags.ts, and the consumer's posthog-server.adapter.ts, posthog-server.ts (EMR frontend), website-posthog-purchase.sink.ts, posthog-hogql.client.ts and feature-flags-cli."}},"servers":[{"url":"https://us.i.posthog.com"}],"paths":{"/flags":{"post":{"operationId":"EvaluateFlags","description":"Remote flag evaluation. \`v=2\` (what every current SDK sends) answers the \`flags\` detail\\nmap; no \`v\` or \`v=1\` answers the legacy \`featureFlags\`/\`featureFlagPayloads\` maps.\\n\`config=true\` adds the remote-config fields. The project key is \`token\` (SDKs) or\\n\`api_key\` (the EMR frontend's raw fetch).\\n","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/Version"},{"$ref":"#/components/parameters/Config"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsRequest"}}}},"responses":{"200":{"description":"Evaluated flags","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsResponse"}}}},"400":{"description":"Malformed body or missing distinct_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"Rate limited (fault preset flags_429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault preset flags_5xx)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/decide":{"post":{"operationId":"Decide","description":"The legacy endpoint (the Python client sends \`POST /decide/?v=3\` with\\n\`{api_key, distinct_id}\`). \`v\` < 4 answers the legacy maps; \`v=4\` the \`flags\` map.\\n","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsRequest"}}}},"responses":{"200":{"description":"Evaluated flags (legacy shape)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsResponse"}}}},"400":{"description":"Malformed body or missing distinct_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"Rate limited (fault preset flags_429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault preset flags_5xx)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/array/{token}/config":{"parameters":[{"$ref":"#/components/parameters/TokenPath"}],"get":{"operationId":"GetRemoteConfig","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Remote config (posthog-react-native, @posthog/core)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoteConfig"}}}}}}},"/array/{token}/config.js":{"parameters":[{"$ref":"#/components/parameters/TokenPath"}],"get":{"operationId":"GetRemoteConfigScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Remote config as a script that sets \`window._POSTHOG_REMOTE_CONFIG[token]\` (posthog-js)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/batch":{"post":{"operationId":"CaptureBatch","description":"posthog-node and posthog-react-native flushes (Content-Encoding: gzip).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/e":{"post":{"operationId":"CaptureEvent","description":"One event, an array of events, or \`{api_key, batch}\` (posthog-js).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/i/v0/e":{"post":{"operationId":"CaptureEventV0","description":"The capture endpoint remote config advertises (\`analytics.endpoint\`); also our backend's website purchase sink (\`{api_key, event, distinct_id, timestamp, uuid, properties}\`).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/s":{"post":{"operationId":"CaptureRecording","description":"Session-recording snapshots; counted, never stored.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}}}}},"/static/recorder.js":{"get":{"operationId":"GetRecorderScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The session recorder bundle (an inert script here)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/static/{version}/recorder.js":{"parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$"}}],"get":{"operationId":"GetVersionedRecorderScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The versioned session recorder bundle (an inert script here)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/api/surveys":{"get":{"operationId":"ListSurveys","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/TokenQuery"}],"responses":{"200":{"description":"No surveys","content":{"application/json":{"schema":{"type":"object","required":["surveys"],"properties":{"surveys":{"type":"array","maxItems":0}}}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/web_experiments":{"get":{"operationId":"ListWebExperiments","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/TokenQuery"}],"responses":{"200":{"description":"No web experiments","content":{"application/json":{"schema":{"type":"object","required":["experiments"],"properties":{"experiments":{"type":"array","maxItems":0}}}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/feature_flags":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"ListFeatureFlags","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":1000}}],"responses":{"200":{"description":"One page of flags (feature-flags-cli follows \`next\`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagPage"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}},"post":{"operationId":"CreateFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeatureFlagBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"400":{"description":"Invalid, or the key already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/feature_flags/{flagId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"flagId","in":"path","required":true,"description":"The flag's numeric id. The mock also accepts the flag key (how self-parity walks reference flags they created, since resource identities are strings).","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"feature_flag","missing":"999999"}}}],"patch":{"operationId":"UpdateFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"400":{"description":"Invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"No such flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/query":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"RunQuery","description":"HogQL (the marketing-metrics crons). Answers canned results set with \`PUT /__admin/settings {\\"queryResults\\": [...]}\`; \`{results: []}\` otherwise.","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"}}}},"responses":{"200":{"description":"Query results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResponse"}}}},"400":{"description":"Not a HogQL query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"Version":{"name":"v","in":"query","schema":{"type":"integer","minimum":1,"maximum":4}},"Config":{"name":"config","in":"query","schema":{"type":"string","enum":["true","false"]}},"TokenPath":{"name":"token","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}},"TokenQuery":{"name":"token","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}},"ProjectId":{"name":"projectId","in":"path","required":true,"schema":{"type":"integer","minimum":1,"maximum":999999999}}},"schemas":{"ErrorBody":{"type":"object","required":["type","code","detail"],"properties":{"type":{"type":"string"},"code":{"type":"string"},"detail":{"type":"string"},"attr":{"type":["string","null"]}}},"FlagsRequest":{"type":"object","required":["distinct_id"],"properties":{"token":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"groups":{"type":"object"},"person_properties":{"type":"object","properties":{"email":{"type":"string","maxLength":120}}},"group_properties":{"type":"object"},"$device_id":{"type":"string","maxLength":64},"$anon_distinct_id":{"type":"string","maxLength":64},"evaluation_contexts":{"type":"array","maxItems":5,"items":{"type":"string","maxLength":40}},"evaluation_runtime":{"type":"string","enum":["server","client","all"]},"flag_keys_to_evaluate":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":64}},"geoip_disable":{"type":"boolean"},"disable_flags":{"type":"boolean"}}},"FlagDetail":{"type":"object","required":["key","enabled","variant","reason","metadata"],"properties":{"key":{"type":"string"},"enabled":{"type":"boolean"},"variant":{"type":["string","null"]},"failed":{"type":"boolean"},"reason":{"type":"object","required":["code","description"],"properties":{"code":{"type":"string"},"condition_index":{"type":["integer","null"]},"description":{"type":"string"}}},"metadata":{"type":"object","required":["id","version"],"properties":{"id":{"type":"integer"},"version":{"type":"integer"},"description":{"type":["string","null"]},"payload":{"type":"string","description":"The payload as a JSON string."}}}}},"FlagsResponse":{"type":"object","required":["errorsWhileComputingFlags"],"properties":{"flags":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/FlagDetail"}},"featureFlags":{"type":"object","additionalProperties":{"type":["boolean","string"]}},"featureFlagPayloads":{"type":"object","additionalProperties":{"type":"string"}},"errorsWhileComputingFlags":{"type":"boolean"},"quotaLimited":{"type":"array","items":{"type":"string"}},"requestId":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"evaluatedAt":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"config":{"type":"object"},"supportedCompression":{"type":"array","items":{"type":"string"}},"sessionRecording":{"oneOf":[{"type":"boolean"},{"type":"object"}]}}},"RemoteConfig":{"type":"object","required":["token","supportedCompression","hasFeatureFlags","analytics","sessionRecording"],"properties":{"token":{"type":"string"},"supportedCompression":{"type":"array","items":{"type":"string"}},"hasFeatureFlags":{"type":"boolean"},"analytics":{"type":"object","required":["endpoint"],"properties":{"endpoint":{"type":"string"}}},"sessionRecording":{"oneOf":[{"type":"boolean"},{"type":"object","required":["endpoint"],"properties":{"endpoint":{"type":"string"}}}]},"surveys":{"type":"boolean"},"heatmaps":{"type":"boolean"},"siteApps":{"type":"array"}}},"CapturedEventBody":{"type":"object","required":["event"],"properties":{"event":{"type":"string","minLength":1,"maxLength":64},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"properties":{"type":"object"},"timestamp":{"type":"string","format":"date-time"},"uuid":{"type":"string","format":"uuid"}}},"BatchRequest":{"type":"object","required":["api_key","batch"],"properties":{"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"batch":{"type":"array","maxItems":5,"items":{"$ref":"#/components/schemas/CapturedEventBody"}},"sent_at":{"type":"string","format":"date-time"}}},"CaptureRequest":{"type":"object","required":["api_key","event"],"properties":{"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"event":{"type":"string","minLength":1,"maxLength":64},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"properties":{"type":"object"},"timestamp":{"type":"string","format":"date-time"},"uuid":{"type":"string","format":"uuid"}}},"CaptureResponse":{"type":"object","required":["status"],"properties":{"status":{"oneOf":[{"type":"string"},{"type":"integer"}]}}},"FeatureFlag":{"type":"object","required":["id","key","name","active","filters"],"properties":{"id":{"type":"integer"},"key":{"type":"string","x-mockingbird-resource":{"type":"feature_flag","identity":true}},"name":{"type":"string"},"active":{"type":"boolean"},"deleted":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"version":{"type":"integer"},"filters":{"type":"object","required":["groups"],"properties":{"groups":{"type":"array","items":{"type":"object"}},"multivariate":{"oneOf":[{"type":"null"},{"type":"object"}]},"payloads":{"type":"object"}}}}},"FeatureFlagPage":{"type":"object","required":["count","next","previous","results"],"properties":{"count":{"type":"integer"},"next":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"previous":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"results":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlag"}}}},"FlagGroup":{"type":"object","properties":{"properties":{"type":"array","maxItems":2,"items":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","enum":["distinct_id","email"]},"type":{"type":"string","enum":["person"]},"operator":{"type":"string","enum":["exact"]},"value":{"type":"array","maxItems":3,"items":{"type":"string","minLength":1,"maxLength":40}}}}},"rollout_percentage":{"type":"integer","minimum":0,"maximum":100},"variant":{"type":["string","null"],"maxLength":20}}},"FlagFilters":{"type":"object","properties":{"groups":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/FlagGroup"}},"payloads":{"type":"object","properties":{"true":{"type":"string","maxLength":200}}}}},"CreateFeatureFlagBody":{"type":"object","required":["key"],"properties":{"key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"name":{"type":"string","maxLength":120},"active":{"type":"boolean"},"filters":{"$ref":"#/components/schemas/FlagFilters"}}},"UpdateFeatureFlagBody":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"active":{"type":"boolean"},"deleted":{"type":"boolean"},"filters":{"$ref":"#/components/schemas/FlagFilters"}}},"QueryRequest":{"type":"object","required":["query"],"properties":{"query":{"type":"object","required":["kind","query"],"properties":{"kind":{"type":"string","enum":["HogQLQuery"]},"query":{"type":"string","minLength":1,"maxLength":500}}}}},"QueryResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"type":"array"}},"columns":{"type":"array","items":{"type":"string"}},"is_cached":{"type":"boolean"}}}}}}`);
3146
- var operationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "UpdateFeatureFlag", "RunQuery"];
3147
- var supportedOperationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "UpdateFeatureFlag", "RunQuery"];
3392
+ var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"PostHog feature flags, capture and management API (Mockingbird subset)","description":"Stateful mock subset of PostHog: remote flag evaluation (\`/flags\` v2 and the legacy\\n\`/decide\` shape), remote config, event capture (\`/batch/\`, \`/e/\`, \`/i/v0/e/\`), session\\nrecording intake, the posthog-js asset and survey endpoints, and the slice of the\\nmanagement API our tooling and crons call (feature-flag list/create/read/patch, experiment management, HogQL query).\\nHand-authored from the wire shapes of posthog-node 5.52.2 / @posthog/core 1.54.0,\\nposthog-js 1.433.2 and our own raw fetches.\\n\\nPaths are written without the trailing slash PostHog uses; the mock answers both\\n(\`/flags/?v=2\` from the SDKs and \`/flags?v=2\` from the EMR frontend's raw fetch).\\nCapture and flag bodies may arrive gzip-compressed (\`Content-Encoding: gzip\`, or raw gzip\\nbytes from posthog-js), base64 (\`data=\` form bodies), or as plain JSON; the contract\\ndeclares the decoded JSON.\\n","version":"1","x-mockingbird-upstream":{"note":"Hand-authored from @posthog/core posthog-core-stateless (getFlags, sendBatch, getRemoteConfig, getSurveysStateless), posthog-js request.ts / posthog-featureflags.ts, and the consumer's posthog-server.adapter.ts, posthog-server.ts (EMR frontend), website-posthog-purchase.sink.ts, posthog-hogql.client.ts and feature-flags-cli."}},"servers":[{"url":"https://us.i.posthog.com"}],"paths":{"/flags":{"post":{"operationId":"EvaluateFlags","description":"Remote flag evaluation. \`v=2\` (what every current SDK sends) answers the \`flags\` detail\\nmap; no \`v\` or \`v=1\` answers the legacy \`featureFlags\`/\`featureFlagPayloads\` maps.\\n\`config=true\` adds the remote-config fields. The project key is \`token\` (SDKs) or\\n\`api_key\` (the EMR frontend's raw fetch).\\n","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/Version"},{"$ref":"#/components/parameters/Config"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsRequest"}}}},"responses":{"200":{"description":"Evaluated flags","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsResponse"}}}},"400":{"description":"Malformed body or missing distinct_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"Rate limited (fault preset flags_429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault preset flags_5xx)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/decide":{"post":{"operationId":"Decide","description":"The legacy endpoint (the Python client sends \`POST /decide/?v=3\` with\\n\`{api_key, distinct_id}\`). \`v\` < 4 answers the legacy maps; \`v=4\` the \`flags\` map.\\n","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsRequest"}}}},"responses":{"200":{"description":"Evaluated flags (legacy shape)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsResponse"}}}},"400":{"description":"Malformed body or missing distinct_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"Rate limited (fault preset flags_429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault preset flags_5xx)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/array/{token}/config":{"parameters":[{"$ref":"#/components/parameters/TokenPath"}],"get":{"operationId":"GetRemoteConfig","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Remote config (posthog-react-native, @posthog/core)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoteConfig"}}}}}}},"/array/{token}/config.js":{"parameters":[{"$ref":"#/components/parameters/TokenPath"}],"get":{"operationId":"GetRemoteConfigScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Remote config as a script that sets \`window._POSTHOG_REMOTE_CONFIG[token]\` (posthog-js)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/batch":{"post":{"operationId":"CaptureBatch","description":"posthog-node and posthog-react-native flushes (Content-Encoding: gzip).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/e":{"post":{"operationId":"CaptureEvent","description":"One event, an array of events, or \`{api_key, batch}\` (posthog-js).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/i/v0/e":{"post":{"operationId":"CaptureEventV0","description":"The capture endpoint remote config advertises (\`analytics.endpoint\`); also our backend's website purchase sink (\`{api_key, event, distinct_id, timestamp, uuid, properties}\`).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/s":{"post":{"operationId":"CaptureRecording","description":"Session-recording snapshots; counted, never stored.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}}}}},"/static/recorder.js":{"get":{"operationId":"GetRecorderScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The session recorder bundle (an inert script here)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/static/{version}/recorder.js":{"parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$"}}],"get":{"operationId":"GetVersionedRecorderScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The versioned session recorder bundle (an inert script here)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/api/surveys":{"get":{"operationId":"ListSurveys","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/TokenQuery"}],"responses":{"200":{"description":"No surveys","content":{"application/json":{"schema":{"type":"object","required":["surveys"],"properties":{"surveys":{"type":"array","maxItems":0}}}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/web_experiments":{"get":{"operationId":"ListWebExperiments","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/TokenQuery"}],"responses":{"200":{"description":"No web experiments","content":{"application/json":{"schema":{"type":"object","required":["experiments"],"properties":{"experiments":{"type":"array","maxItems":0}}}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/feature_flags":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"ListFeatureFlags","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":1000}}],"responses":{"200":{"description":"One page of flags (feature-flags-cli follows \`next\`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagPage"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}},"post":{"operationId":"CreateFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeatureFlagBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"400":{"description":"Invalid, or the key already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/feature_flags/{flagId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"flagId","in":"path","required":true,"description":"The flag's numeric id. The mock also accepts the flag key (how self-parity walks reference flags they created, since resource identities are strings).","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"feature_flag","missing":"999999"}}}],"get":{"operationId":"GetFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Current flag by numeric id (or mock key alias)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"401":{"$ref":"#/components/responses/ManagementError"},"404":{"$ref":"#/components/responses/ManagementError"}}},"patch":{"operationId":"UpdateFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"400":{"description":"Invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"No such flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/experiments":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"ListExperiments","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":1000,"default":0}}],"responses":{"200":{"description":"Newest experiments first, with limit/offset links","content":{"application/json":{"schema":{"type":"object","required":["count","next","previous","results"],"properties":{"count":{"type":"integer"},"next":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"previous":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"results":{"type":"array","items":{"$ref":"#/components/schemas/Experiment"}}}}}}},"400":{"$ref":"#/components/responses/ManagementError"},"401":{"$ref":"#/components/responses/ManagementError"}}},"post":{"operationId":"CreateExperiment","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ExperimentWrite"},{"type":"object","required":["name","feature_flag_key"],"properties":{"feature_flag_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}}}]}}}},"responses":{"201":{"description":"Experiment with a linked flag; new draft flags are inactive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Experiment"}}}},"400":{"$ref":"#/components/responses/ManagementError"},"401":{"$ref":"#/components/responses/ManagementError"}}}},"/api/projects/{projectId}/experiments/{experimentId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"experimentId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{1,6}$"}}],"get":{"operationId":"GetExperiment","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Persisted experiment with the current linked flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Experiment"}}}},"401":{"$ref":"#/components/responses/ManagementError"},"404":{"$ref":"#/components/responses/ManagementError"}}},"patch":{"operationId":"UpdateExperiment","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentWrite"}}}},"responses":{"200":{"description":"Only provided fields change; setting start_date activates a draft's flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Experiment"}}}},"400":{"$ref":"#/components/responses/ManagementError"},"401":{"$ref":"#/components/responses/ManagementError"},"404":{"$ref":"#/components/responses/ManagementError"}}}},"/api/projects/{projectId}/query":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"RunQuery","description":"HogQL (the marketing-metrics crons). Answers canned results set with \`PUT /__admin/settings {\\"queryResults\\": [...]}\`; \`{results: []}\` otherwise.","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"}}}},"responses":{"200":{"description":"Query results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResponse"}}}},"400":{"description":"Not a HogQL query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}}},"components":{"responses":{"ManagementError":{"description":"Management API error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"Version":{"name":"v","in":"query","schema":{"type":"integer","minimum":1,"maximum":4}},"Config":{"name":"config","in":"query","schema":{"type":"string","enum":["true","false"]}},"TokenPath":{"name":"token","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}},"TokenQuery":{"name":"token","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}},"ProjectId":{"name":"projectId","in":"path","required":true,"schema":{"type":"integer","minimum":1,"maximum":999999999}}},"schemas":{"ExperimentWrite":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"parameters":{"type":"object"},"metrics":{"type":"array","maxItems":5,"items":{"type":"object"}},"start_date":{"type":["string","null"],"format":"date-time"},"end_date":{"type":["string","null"],"format":"date-time"},"archived":{"type":"boolean"}}},"Experiment":{"type":"object","required":["id","name","feature_flag_key","feature_flag","parameters","metrics","start_date","end_date","archived","created_at","updated_at"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"feature_flag_key":{"type":"string"},"feature_flag":{"oneOf":[{"$ref":"#/components/schemas/FeatureFlag"},{"type":"null"}]},"parameters":{"type":"object"},"metrics":{"type":"array","items":{"type":"object"}},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]},"archived":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"ErrorBody":{"type":"object","required":["type","code","detail"],"properties":{"type":{"type":"string"},"code":{"type":"string"},"detail":{"type":"string"},"attr":{"type":["string","null"]}}},"FlagsRequest":{"type":"object","required":["distinct_id"],"properties":{"token":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"groups":{"type":"object"},"person_properties":{"type":"object","properties":{"email":{"type":"string","maxLength":120}}},"group_properties":{"type":"object"},"$device_id":{"type":"string","maxLength":64},"$anon_distinct_id":{"type":"string","maxLength":64},"evaluation_contexts":{"type":"array","maxItems":5,"items":{"type":"string","maxLength":40}},"evaluation_runtime":{"type":"string","enum":["server","client","all"]},"flag_keys_to_evaluate":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":64}},"geoip_disable":{"type":"boolean"},"disable_flags":{"type":"boolean"}}},"FlagDetail":{"type":"object","required":["key","enabled","variant","reason","metadata"],"properties":{"key":{"type":"string"},"enabled":{"type":"boolean"},"variant":{"type":["string","null"]},"failed":{"type":"boolean"},"reason":{"type":"object","required":["code","description"],"properties":{"code":{"type":"string"},"condition_index":{"type":["integer","null"]},"description":{"type":"string"}}},"metadata":{"type":"object","required":["id","version"],"properties":{"id":{"type":"integer"},"version":{"type":"integer"},"description":{"type":["string","null"]},"payload":{"type":"string","description":"The payload as a JSON string."}}}}},"FlagsResponse":{"type":"object","required":["errorsWhileComputingFlags"],"properties":{"flags":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/FlagDetail"}},"featureFlags":{"type":"object","additionalProperties":{"type":["boolean","string"]}},"featureFlagPayloads":{"type":"object","additionalProperties":{"type":"string"}},"errorsWhileComputingFlags":{"type":"boolean"},"quotaLimited":{"type":"array","items":{"type":"string"}},"requestId":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"evaluatedAt":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"config":{"type":"object"},"supportedCompression":{"type":"array","items":{"type":"string"}},"sessionRecording":{"oneOf":[{"type":"boolean"},{"type":"object"}]}}},"RemoteConfig":{"type":"object","required":["token","supportedCompression","hasFeatureFlags","analytics","sessionRecording"],"properties":{"token":{"type":"string"},"supportedCompression":{"type":"array","items":{"type":"string"}},"hasFeatureFlags":{"type":"boolean"},"analytics":{"type":"object","required":["endpoint"],"properties":{"endpoint":{"type":"string"}}},"sessionRecording":{"oneOf":[{"type":"boolean"},{"type":"object","required":["endpoint"],"properties":{"endpoint":{"type":"string"}}}]},"surveys":{"type":"boolean"},"heatmaps":{"type":"boolean"},"siteApps":{"type":"array"}}},"CapturedEventBody":{"type":"object","required":["event"],"properties":{"event":{"type":"string","minLength":1,"maxLength":64},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"properties":{"type":"object"},"timestamp":{"type":"string","format":"date-time"},"uuid":{"type":"string","format":"uuid"}}},"BatchRequest":{"type":"object","required":["api_key","batch"],"properties":{"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"batch":{"type":"array","maxItems":5,"items":{"$ref":"#/components/schemas/CapturedEventBody"}},"sent_at":{"type":"string","format":"date-time"}}},"CaptureRequest":{"type":"object","required":["api_key","event"],"properties":{"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"event":{"type":"string","minLength":1,"maxLength":64},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"properties":{"type":"object"},"timestamp":{"type":"string","format":"date-time"},"uuid":{"type":"string","format":"uuid"}}},"CaptureResponse":{"type":"object","required":["status"],"properties":{"status":{"oneOf":[{"type":"string"},{"type":"integer"}]}}},"FeatureFlag":{"type":"object","required":["id","key","name","active","filters"],"properties":{"id":{"type":"integer"},"key":{"type":"string","x-mockingbird-resource":{"type":"feature_flag","identity":true}},"name":{"type":"string"},"active":{"type":"boolean"},"deleted":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"version":{"type":"integer"},"filters":{"type":"object","required":["groups"],"properties":{"groups":{"type":"array","items":{"type":"object"}},"multivariate":{"oneOf":[{"type":"null"},{"type":"object"}]},"payloads":{"type":"object"}}}}},"FeatureFlagPage":{"type":"object","required":["count","next","previous","results"],"properties":{"count":{"type":"integer"},"next":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"previous":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"results":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlag"}}}},"FlagGroup":{"type":"object","properties":{"properties":{"type":"array","maxItems":2,"items":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","enum":["distinct_id","email"]},"type":{"type":"string","enum":["person"]},"operator":{"type":"string","enum":["exact"]},"value":{"type":"array","maxItems":3,"items":{"type":"string","minLength":1,"maxLength":40}}}}},"rollout_percentage":{"type":"integer","minimum":0,"maximum":100},"variant":{"type":["string","null"],"maxLength":20}}},"FlagFilters":{"type":"object","properties":{"groups":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/FlagGroup"}},"payloads":{"type":"object","properties":{"true":{"type":"string","maxLength":200}}}}},"CreateFeatureFlagBody":{"type":"object","required":["key"],"properties":{"key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"name":{"type":"string","maxLength":120},"active":{"type":"boolean"},"filters":{"$ref":"#/components/schemas/FlagFilters"}}},"UpdateFeatureFlagBody":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"active":{"type":"boolean"},"deleted":{"type":"boolean"},"filters":{"$ref":"#/components/schemas/FlagFilters"}}},"QueryRequest":{"type":"object","required":["query"],"properties":{"query":{"type":"object","required":["kind","query"],"properties":{"kind":{"type":"string","enum":["HogQLQuery"]},"query":{"type":"string","minLength":1,"maxLength":500}}}}},"QueryResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"type":"array"}},"columns":{"type":"array","items":{"type":"string"}},"is_cached":{"type":"boolean"}}}}}}`);
3393
+ var operationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "GetFeatureFlag", "UpdateFeatureFlag", "ListExperiments", "CreateExperiment", "GetExperiment", "UpdateExperiment", "RunQuery"];
3394
+ var supportedOperationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "GetFeatureFlag", "UpdateFeatureFlag", "ListExperiments", "CreateExperiment", "GetExperiment", "UpdateExperiment", "RunQuery"];
3148
3395
 
3149
3396
  // src/state.ts
3150
3397
  var DEFAULT_SETTINGS = {
@@ -3158,6 +3405,7 @@ var PostHogState = class {
3158
3405
  this.seed = seed;
3159
3406
  this.now = now;
3160
3407
  this.flags = new Collection(sqlite, namespace, "flags");
3408
+ this.experiments = new Collection(sqlite, namespace, "experiments");
3161
3409
  this.events = new Collection(sqlite, namespace, "events");
3162
3410
  this.settings = new Collection(sqlite, namespace, "settings");
3163
3411
  this.ids = new IdSequence(sqlite, namespace, "posthog");
@@ -3166,6 +3414,7 @@ var PostHogState = class {
3166
3414
  seed;
3167
3415
  now;
3168
3416
  flags;
3417
+ experiments;
3169
3418
  events;
3170
3419
  settings;
3171
3420
  ids;
@@ -3207,6 +3456,7 @@ var PostHogState = class {
3207
3456
  default: spec.default,
3208
3457
  payload: spec.payload,
3209
3458
  overrides: spec.overrides,
3459
+ ...spec.filters ? { filters: spec.filters } : {},
3210
3460
  version: (existing?.version ?? 0) + 1,
3211
3461
  created_at: existing?.created_at ?? iso,
3212
3462
  updated_at: iso
@@ -3237,9 +3487,9 @@ var ACME_FLAG_STATE = JSON.parse(
3237
3487
  );
3238
3488
 
3239
3489
  // src/import.ts
3240
- var valueForState = (state, variants = []) => {
3490
+ var valueForState = (state, variants2 = []) => {
3241
3491
  if (state === "live") {
3242
- const top = [...variants].sort((a, b) => b.rolloutPercentage - a.rolloutPercentage)[0];
3492
+ const top = [...variants2].sort((a, b) => b.rolloutPercentage - a.rolloutPercentage)[0];
3243
3493
  return top?.variant ?? true;
3244
3494
  }
3245
3495
  if (state === "rollout 0" || state === "targeted" || state === "ramping") return false;
@@ -3310,11 +3560,11 @@ var POSTHOG_PRESETS = {
3310
3560
  };
3311
3561
  var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
3312
3562
  var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
3313
- var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3563
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3314
3564
  var parseBulk = (body) => {
3315
- const source = isRecord7(body) && isRecord7(body.flags) ? body.flags : body;
3316
- const entries = Array.isArray(source) ? source.map((each) => [isRecord7(each) ? String(each.key ?? "") : "", each]) : isRecord7(source) ? Object.entries(source).filter(([key]) => key !== "replace") : [];
3317
- if (!Array.isArray(source) && !isRecord7(source)) return "expected {flags: {<key>: spec}}";
3565
+ const source = isRecord8(body) && isRecord8(body.flags) ? body.flags : body;
3566
+ const entries = Array.isArray(source) ? source.map((each) => [isRecord8(each) ? String(each.key ?? "") : "", each]) : isRecord8(source) ? Object.entries(source).filter(([key]) => key !== "replace") : [];
3567
+ if (!Array.isArray(source) && !isRecord8(source)) return "expected {flags: {<key>: spec}}";
3318
3568
  const specs = {};
3319
3569
  for (const [key, value] of entries) {
3320
3570
  if (!key) return "every flag needs a key";
@@ -3328,7 +3578,7 @@ var parseQueryResults = (value) => {
3328
3578
  if (!Array.isArray(value)) return "queryResults: [{match?, columns?, results: [[\u2026]]}]";
3329
3579
  const out = [];
3330
3580
  for (const each of value) {
3331
- if (!isRecord7(each) || !Array.isArray(each.results) || !each.results.every(Array.isArray)) {
3581
+ if (!isRecord8(each) || !Array.isArray(each.results) || !each.results.every(Array.isArray)) {
3332
3582
  return "each queryResults entry needs results: unknown[][]";
3333
3583
  }
3334
3584
  out.push({
@@ -3369,19 +3619,19 @@ var adminRoutes = (runtime) => {
3369
3619
  const specs = parseBulk(body);
3370
3620
  if (typeof specs === "string") return adminError4(400, specs);
3371
3621
  const state = api(namespace).state;
3372
- if (isRecord7(body) && body.replace === true) {
3622
+ if (isRecord8(body) && body.replace === true) {
3373
3623
  for (const flag of state.listFlags()) state.flags.delete(flag.key);
3374
3624
  }
3375
3625
  for (const [key, spec] of Object.entries(specs)) state.putFlag(key, spec);
3376
3626
  return json5(200, { flags: api(namespace).flagList() });
3377
3627
  },
3378
3628
  "POST /flags/import": ({ body, namespace }) => {
3379
- const input = isRecord7(body) ? body : {};
3629
+ const input = isRecord8(body) ? body : {};
3380
3630
  const env = input.env ?? "dev";
3381
3631
  if (env !== "dev" && env !== "prod") return adminError4(400, 'env must be "dev" or "prod"');
3382
3632
  const from = input.from ?? "state.json";
3383
3633
  let file;
3384
- if (isRecord7(input.state) && Array.isArray(input.state.flags)) {
3634
+ if (isRecord8(input.state) && Array.isArray(input.state.flags)) {
3385
3635
  file = input.state;
3386
3636
  } else if (from === "state.json") {
3387
3637
  file = ACME_FLAG_STATE;
@@ -3425,7 +3675,7 @@ var adminRoutes = (runtime) => {
3425
3675
  "GET /recordings": ({ namespace }) => json5(200, { count: api(namespace).state.current().recordings }),
3426
3676
  "GET /settings": ({ namespace }) => json5(200, api(namespace).state.current()),
3427
3677
  "PUT /settings": ({ body, namespace }) => {
3428
- if (!isRecord7(body)) return adminError4(400, "expected a JSON object");
3678
+ if (!isRecord8(body)) return adminError4(400, "expected a JSON object");
3429
3679
  const patch = {};
3430
3680
  if (body.sessionRecording !== void 0) {
3431
3681
  if (typeof body.sessionRecording !== "boolean")
@@ -3454,9 +3704,10 @@ var createRuntime2 = (options = {}) => {
3454
3704
  ...options.onLog ? { onLog: options.onLog } : {},
3455
3705
  credential: (request) => tokens.get(request),
3456
3706
  presets: POSTHOG_PRESETS,
3457
- create: ({ sqlite, namespace, clock }) => new PostHogAPI({
3707
+ create: ({ sqlite, namespace, clock, adminPrefix }) => new PostHogAPI({
3458
3708
  sqlite,
3459
3709
  namespace,
3710
+ adminPrefix,
3460
3711
  now: clock.now,
3461
3712
  ...options.flags ? { flags: options.flags } : {},
3462
3713
  ...options.settings ? { settings: options.settings } : {}
@@ -3478,15 +3729,15 @@ var createRuntime2 = (options = {}) => {
3478
3729
 
3479
3730
  // src/index.ts
3480
3731
  var POSTHOG_NAMESPACE = "posthog";
3481
- var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3482
- var error = (status, type, code, detail, attr) => jsonRes(status, { type, code, detail, attr: attr ?? null });
3483
- var invalidApiKey = () => error(
3732
+ var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3733
+ var error2 = (status, type, code, detail, attr) => jsonRes(status, { type, code, detail, attr: attr ?? null });
3734
+ var invalidApiKey = () => error2(
3484
3735
  401,
3485
3736
  "authentication_error",
3486
3737
  "invalid_api_key",
3487
3738
  "Project API key invalid. You can find your project API key in your PostHog project settings."
3488
3739
  );
3489
- var malformed = (detail = "Malformed request data") => error(400, "validation_error", "invalid_payload", detail);
3740
+ var malformed = (detail = "Malformed request data") => error2(400, "validation_error", "invalid_payload", detail);
3490
3741
  var remoteConfig = (token, settings) => ({
3491
3742
  token,
3492
3743
  supportedCompression: ["gzip", "gzip-js"],
@@ -3516,7 +3767,7 @@ var scrubProperties = (event, properties) => {
3516
3767
  }
3517
3768
  const kept = scrubRecord(properties);
3518
3769
  for (const nested of ["$set", "$set_once"]) {
3519
- if (isRecord8(kept[nested])) kept[nested] = scrubRecord(kept[nested]);
3770
+ if (isRecord9(kept[nested])) kept[nested] = scrubRecord(kept[nested]);
3520
3771
  }
3521
3772
  return kept;
3522
3773
  };
@@ -3566,6 +3817,11 @@ var PostHogAPI = class {
3566
3817
  GetVersionedRecorderScript: () => script("/* mockingbird: session recording is not modelled */\n"),
3567
3818
  ListSurveys: (context) => typeof context.query.token === "string" && context.query.token ? jsonRes(200, { surveys: [] }) : invalidApiKey(),
3568
3819
  ListWebExperiments: (context) => typeof context.query.token === "string" && context.query.token ? jsonRes(200, { experiments: [] }) : invalidApiKey(),
3820
+ ...experimentOperations(this.state, this.now, options.adminPrefix ?? "/__admin"),
3821
+ GetFeatureFlag: (context) => {
3822
+ const flag = this.state.findFlag(context.params.flagId ?? "");
3823
+ return flag && !flag.deleted ? jsonRes(200, restView(flag)) : error2(404, "invalid_request", "not_found", "Not found.");
3824
+ },
3569
3825
  ListFeatureFlags: (context) => this.listFlags(context),
3570
3826
  CreateFeatureFlag: (context) => this.createFlag(context),
3571
3827
  UpdateFeatureFlag: (context) => this.updateFlag(context),
@@ -3577,7 +3833,7 @@ var PostHogAPI = class {
3577
3833
  sqlite,
3578
3834
  namespace,
3579
3835
  now: this.now,
3580
- notFound: () => error(404, "invalid_request", "not_found", "Not found."),
3836
+ notFound: () => error2(404, "invalid_request", "not_found", "Not found."),
3581
3837
  onError: (thrown) => {
3582
3838
  if (thrown instanceof HttpError) return thrown.toResponse();
3583
3839
  throw thrown;
@@ -3585,7 +3841,7 @@ var PostHogAPI = class {
3585
3841
  before: (context) => {
3586
3842
  if (!context.url.pathname.startsWith("/api/projects/")) return void 0;
3587
3843
  if (bearerToken(context.request)) return void 0;
3588
- return error(
3844
+ return error2(
3589
3845
  401,
3590
3846
  "authentication_error",
3591
3847
  "not_authenticated",
@@ -3637,17 +3893,17 @@ var PostHogAPI = class {
3637
3893
  }
3638
3894
  flags(context, route) {
3639
3895
  const body = this.body(context);
3640
- if (!isRecord8(body)) return malformed();
3896
+ if (!isRecord9(body)) return malformed();
3641
3897
  if (!tokenFromBody(body)) return invalidApiKey();
3642
3898
  const distinctId = body.distinct_id;
3643
3899
  if (typeof distinctId !== "string" && typeof distinctId !== "number") {
3644
- return error(400, "validation_error", "missing_distinct_id", "Decide requires a distinct_id.");
3900
+ return error2(400, "validation_error", "missing_distinct_id", "Decide requires a distinct_id.");
3645
3901
  }
3646
3902
  const version = Number(context.url.searchParams.get("v") ?? (route === "decide" ? 3 : 1));
3647
3903
  const detailed = route === "decide" ? version >= 4 : version >= 2;
3648
3904
  const subject = {
3649
3905
  distinct_id: String(distinctId),
3650
- ...isRecord8(body.person_properties) ? { person_properties: body.person_properties } : {}
3906
+ ...isRecord9(body.person_properties) ? { person_properties: body.person_properties } : {}
3651
3907
  };
3652
3908
  const keys = [body.flag_keys_to_evaluate, body.flag_keys].find(Array.isArray);
3653
3909
  const quotaLimited = this.effect(context, "quota_limited");
@@ -3680,15 +3936,15 @@ var PostHogAPI = class {
3680
3936
  }
3681
3937
  const token = tokenFromBody(body);
3682
3938
  if (!token) return invalidApiKey();
3683
- const items = Array.isArray(body) ? body : isRecord8(body) && Array.isArray(body.batch) ? body.batch : [body];
3939
+ const items = Array.isArray(body) ? body : isRecord9(body) && Array.isArray(body.batch) ? body.batch : [body];
3684
3940
  const events = items.filter(
3685
- (item) => isRecord8(item) && typeof item.event === "string" && item.event.length > 0
3941
+ (item) => isRecord9(item) && typeof item.event === "string" && item.event.length > 0
3686
3942
  );
3687
3943
  if (events.length === 0 && items.length > 0) return malformed("Invalid payload: no event name");
3688
3944
  const stored = [];
3689
3945
  for (const raw of events) {
3690
3946
  const event = raw.event;
3691
- const properties = isRecord8(raw.properties) ? raw.properties : {};
3947
+ const properties = isRecord9(raw.properties) ? raw.properties : {};
3692
3948
  const distinct = raw.distinct_id ?? properties.distinct_id ?? raw.$distinct_id ?? properties.$distinct_id;
3693
3949
  const uuid = typeof raw.uuid === "string" && raw.uuid ? raw.uuid : this.state.nextEventId();
3694
3950
  if (this.state.events.has(uuid)) continue;
@@ -3730,10 +3986,10 @@ var PostHogAPI = class {
3730
3986
  }
3731
3987
  createFlag(context) {
3732
3988
  const body = this.body(context);
3733
- if (!isRecord8(body)) return malformed();
3989
+ if (!isRecord9(body)) return malformed();
3734
3990
  const key = body.key;
3735
3991
  if (typeof key !== "string" || !/^[A-Za-z0-9_-]+$/.test(key)) {
3736
- return error(
3992
+ return error2(
3737
3993
  400,
3738
3994
  "validation_error",
3739
3995
  "invalid_input",
@@ -3743,7 +3999,7 @@ var PostHogAPI = class {
3743
3999
  }
3744
4000
  const existing = this.state.flags.get(key);
3745
4001
  if (existing && !existing.deleted) {
3746
- return error(
4002
+ return error2(
3747
4003
  400,
3748
4004
  "validation_error",
3749
4005
  "unique",
@@ -3760,22 +4016,29 @@ var PostHogAPI = class {
3760
4016
  }
3761
4017
  updateFlag(context) {
3762
4018
  const flag = this.state.findFlag(context.params.flagId ?? "");
3763
- if (!flag) return error(404, "invalid_request", "not_found", "Not found.");
4019
+ if (!flag) return error2(404, "invalid_request", "not_found", "Not found.");
3764
4020
  const body = this.body(context);
3765
- if (!isRecord8(body)) return malformed();
4021
+ if (!isRecord9(body)) return malformed();
3766
4022
  const patch = {};
3767
4023
  if (typeof body.name === "string") patch.name = body.name;
3768
4024
  if (typeof body.active === "boolean") patch.active = body.active;
3769
4025
  if (typeof body.deleted === "boolean") patch.deleted = body.deleted;
3770
- if (body.filters !== void 0) Object.assign(patch, fromFilters(body.filters));
4026
+ if (body.filters !== void 0) {
4027
+ Object.assign(patch, fromFilters(body.filters));
4028
+ if (flag.filters && isRecord9(body.filters))
4029
+ patch.filters = {
4030
+ ...body.filters,
4031
+ groups: Array.isArray(body.filters.groups) ? body.filters.groups : []
4032
+ };
4033
+ }
3771
4034
  const updated = this.state.patchFlag(flag.key, patch) ?? flag;
3772
4035
  return annotateResponse(jsonRes(200, restView(updated)), { ids: { flag: flag.key } });
3773
4036
  }
3774
4037
  query(context) {
3775
4038
  const body = this.body(context);
3776
- const query = isRecord8(body) && isRecord8(body.query) ? body.query : void 0;
4039
+ const query = isRecord9(body) && isRecord9(body.query) ? body.query : void 0;
3777
4040
  if (query?.kind !== "HogQLQuery" || typeof query.query !== "string") {
3778
- return error(400, "validation_error", "invalid_input", "Expected a HogQLQuery.", "query");
4041
+ return error2(400, "validation_error", "invalid_input", "Expected a HogQLQuery.", "query");
3779
4042
  }
3780
4043
  const text = query.query;
3781
4044
  const canned = this.state.current().queryResults.find((each) => each.match === void 0 || text.includes(each.match));
@@ -3818,4 +4081,4 @@ export {
3818
4081
  scrubProperties,
3819
4082
  PostHogAPI
3820
4083
  };
3821
- //# sourceMappingURL=chunk-VZ3ZHVNW.js.map
4084
+ //# sourceMappingURL=chunk-SOC56HS3.js.map