@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.
- package/CHANGELOG.md +12 -4
- package/README.md +5 -3
- package/SUPPORT.md +8 -3
- package/dist/{chunk-SCY2VOTE.js → chunk-DSIEW2HP.js} +2 -2
- package/dist/{chunk-VZ3ZHVNW.js → chunk-SOC56HS3.js} +327 -64
- package/dist/chunk-SOC56HS3.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +23 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +18 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +168 -1
- package/package.json +1 -1
- package/dist/chunk-VZ3ZHVNW.js.map +0 -7
- /package/dist/{chunk-SCY2VOTE.js.map → chunk-DSIEW2HP.js.map} +0 -0
|
@@ -251,8 +251,8 @@ var createControlPlane = (context) => {
|
|
|
251
251
|
preset,
|
|
252
252
|
rules: context.applyPreset(preset, namespace, overrides)
|
|
253
253
|
});
|
|
254
|
-
} catch (
|
|
255
|
-
return adminError(404,
|
|
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 (
|
|
311
|
-
return adminError(409,
|
|
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 (
|
|
322
|
-
return adminError(409,
|
|
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 (
|
|
336
|
-
return adminError(409,
|
|
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 (
|
|
813
|
+
} catch (error3) {
|
|
814
814
|
return {
|
|
815
815
|
kind: "invalid",
|
|
816
816
|
mediaType,
|
|
817
817
|
text,
|
|
818
|
-
error:
|
|
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
|
-
|
|
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
|
|
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((
|
|
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:
|
|
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(),
|
|
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 (
|
|
1940
|
-
return fail(400,
|
|
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.
|
|
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 (
|
|
2725
|
+
} catch (error3) {
|
|
2657
2726
|
log(409);
|
|
2658
|
-
return stamp(adminFail(409,
|
|
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
|
|
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:
|
|
3096
|
-
variants:
|
|
3164
|
+
multivariate: variants2.length > 0 ? {
|
|
3165
|
+
variants: variants2.map((key, index) => ({
|
|
3097
3166
|
key,
|
|
3098
|
-
rollout_percentage: Math.floor(100 /
|
|
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
|
|
3111
|
-
const topVariant = [...
|
|
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,
|
|
3490
|
+
var valueForState = (state, variants2 = []) => {
|
|
3241
3491
|
if (state === "live") {
|
|
3242
|
-
const top = [...
|
|
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
|
|
3563
|
+
var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3314
3564
|
var parseBulk = (body) => {
|
|
3315
|
-
const source =
|
|
3316
|
-
const entries = Array.isArray(source) ? source.map((each) => [
|
|
3317
|
-
if (!Array.isArray(source) && !
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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
|
|
3482
|
-
var
|
|
3483
|
-
var invalidApiKey = () =>
|
|
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") =>
|
|
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 (
|
|
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: () =>
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
-
...
|
|
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 :
|
|
3939
|
+
const items = Array.isArray(body) ? body : isRecord9(body) && Array.isArray(body.batch) ? body.batch : [body];
|
|
3684
3940
|
const events = items.filter(
|
|
3685
|
-
(item) =>
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
4019
|
+
if (!flag) return error2(404, "invalid_request", "not_found", "Not found.");
|
|
3764
4020
|
const body = this.body(context);
|
|
3765
|
-
if (!
|
|
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)
|
|
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 =
|
|
4039
|
+
const query = isRecord9(body) && isRecord9(body.query) ? body.query : void 0;
|
|
3777
4040
|
if (query?.kind !== "HogQLQuery" || typeof query.query !== "string") {
|
|
3778
|
-
return
|
|
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-
|
|
4084
|
+
//# sourceMappingURL=chunk-SOC56HS3.js.map
|