Files
zooqueen 252e1cb076 Generated clients call the API where it is mounted
`insights/mount.py` is explicit: `/v1/` is the mount and `/api/` is the older
spelling, kept on the wire for released CLI builds and personal-API-key
holders and rewritten once at the edge by `ApiRewriteMiddleware`. Our own
frontend was still on the older spelling: the orval clients were generated
two days before the mount moved and the move did not regenerate them, so 70
files and 2323 paths said `/api/`.

That is not only a spelling. `prepareUrl` in `lib/api.ts` rewrites `/v1/*` to
the selected region's host in OAuth mode and nothing else, because that is
what the cloud CORS allowlist covers — so every generated call quietly stayed
same-origin, and `getExportsContentRetrieveUrl` builds an `<img src>` that
never reaches `lib/api.ts` at all.

The clients are regenerated from a spec that needs postgres, the datastore and
Django, so this is the path strings only, which is the whole of what the mount
move changes in them: orval copies paths verbatim from the spec, and the spec
takes them from the URLconf. Doc comments carried the same stale prefix and
move with them; their Python docstrings already say `/v1/`. The two frozen
OAuth client-metadata paths are identifiers rather than API surface and appear
in none of these files.

The Slack task prompt told the agent to POST to
`$INSIGHTS_API_URL/api/projects/...`, which is a live request path, not prose.
2026-08-24 23:13:08 -07:00

1176 lines
43 KiB
TypeScript
Generated

import { apiMutator } from '../../../../frontend/src/lib/api-orval-mutator'
/**
* Auto-generated from the Django backend OpenAPI schema.
* To modify these types, update the Django serializers or views, then run:
* insightscli build:openapi
* Questions or issues? #team-devex on Slack
*
* Insights API - generated
* OpenAPI spec version: 1.0.0
*/
import type {
ActivityLogPaginatedResponseApi,
ArchiveExperimentApi,
CopyExperimentToProjectApi,
CreateFromPromptInputApi,
EndExperimentApi,
ExperimentApi,
ExperimentFlagCleanupTargetApi,
ExperimentFlagCleanupTaskApi,
ExperimentHoldoutApi,
ExperimentHoldoutsListParams,
ExperimentMetricsRecalculationApi,
ExperimentSavedMetricApi,
ExperimentSavedMetricsListParams,
ExperimentSessionBucketRequestApi,
ExperimentSessionBucketResponseApi,
ExperimentSessionContextResponseApi,
ExperimentSessionContextsRequestApi,
ExperimentSessionContextsResponseApi,
ExperimentWriteApi,
ExperimentsActivityRetrieveParams,
ExperimentsListParams,
ExperimentsPromptTemplatesRetrieve200Item,
ExperimentsSessionContextRetrieveParams,
ExperimentsTimeseriesResultsRetrieveParams,
PaginatedExperimentBasicListApi,
PaginatedExperimentHoldoutListApi,
PaginatedExperimentSavedMetricListApi,
PatchedExperimentHoldoutApi,
PatchedExperimentSavedMetricApi,
PatchedExperimentWriteApi,
RecalculateMetricsRequestApi,
RunningTimeCalculationInputApi,
RunningTimeCalculationResultApi,
ShipVariantApi,
} from './api.schemas'
// https://stackoverflow.com/questions/49579094/typescript-conditional-types-filter-out-readonly-properties-pick-only-requir/49579497#49579497
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B
type WritableKeys<T> = {
[P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T]
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
type DistributeReadOnlyOverUnions<T> = T extends any ? NonReadonly<T> : never
type Writable<T> = Pick<T, WritableKeys<T>>
type NonReadonly<T> = [T] extends [UnionToIntersection<T>]
? {
[P in keyof Writable<T>]: T[P] extends object ? NonReadonly<NonNullable<T[P]>> : T[P]
}
: DistributeReadOnlyOverUnions<T>
export const getExperimentHoldoutsListUrl = (projectId: string, params?: ExperimentHoldoutsListParams) => {
const normalizedParams = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : String(value))
}
})
const stringifiedParams = normalizedParams.toString()
return stringifiedParams.length > 0
? `/v1/projects/${projectId}/experiment_holdouts/?${stringifiedParams}`
: `/v1/projects/${projectId}/experiment_holdouts/`
}
export const experimentHoldoutsList = async (
projectId: string,
params?: ExperimentHoldoutsListParams,
options?: RequestInit
): Promise<PaginatedExperimentHoldoutListApi> => {
return apiMutator<PaginatedExperimentHoldoutListApi>(getExperimentHoldoutsListUrl(projectId, params), {
...options,
method: 'GET',
})
}
export const getExperimentHoldoutsCreateUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiment_holdouts/`
}
export const experimentHoldoutsCreate = async (
projectId: string,
experimentHoldoutApi: NonReadonly<ExperimentHoldoutApi>,
options?: RequestInit
): Promise<ExperimentHoldoutApi> => {
return apiMutator<ExperimentHoldoutApi>(getExperimentHoldoutsCreateUrl(projectId), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentHoldoutApi),
})
}
export const getExperimentHoldoutsRetrieveUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_holdouts/${id}/`
}
export const experimentHoldoutsRetrieve = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentHoldoutApi> => {
return apiMutator<ExperimentHoldoutApi>(getExperimentHoldoutsRetrieveUrl(projectId, id), {
...options,
method: 'GET',
})
}
export const getExperimentHoldoutsUpdateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_holdouts/${id}/`
}
export const experimentHoldoutsUpdate = async (
projectId: string,
id: number,
experimentHoldoutApi: NonReadonly<ExperimentHoldoutApi>,
options?: RequestInit
): Promise<ExperimentHoldoutApi> => {
return apiMutator<ExperimentHoldoutApi>(getExperimentHoldoutsUpdateUrl(projectId, id), {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentHoldoutApi),
})
}
export const getExperimentHoldoutsPartialUpdateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_holdouts/${id}/`
}
export const experimentHoldoutsPartialUpdate = async (
projectId: string,
id: number,
patchedExperimentHoldoutApi?: NonReadonly<PatchedExperimentHoldoutApi>,
options?: RequestInit
): Promise<ExperimentHoldoutApi> => {
return apiMutator<ExperimentHoldoutApi>(getExperimentHoldoutsPartialUpdateUrl(projectId, id), {
...options,
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(patchedExperimentHoldoutApi),
})
}
export const getExperimentHoldoutsDestroyUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_holdouts/${id}/`
}
export const experimentHoldoutsDestroy = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<void> => {
return apiMutator<void>(getExperimentHoldoutsDestroyUrl(projectId, id), {
...options,
method: 'DELETE',
})
}
export const getExperimentSavedMetricsListUrl = (projectId: string, params?: ExperimentSavedMetricsListParams) => {
const normalizedParams = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : String(value))
}
})
const stringifiedParams = normalizedParams.toString()
return stringifiedParams.length > 0
? `/v1/projects/${projectId}/experiment_saved_metrics/?${stringifiedParams}`
: `/v1/projects/${projectId}/experiment_saved_metrics/`
}
export const experimentSavedMetricsList = async (
projectId: string,
params?: ExperimentSavedMetricsListParams,
options?: RequestInit
): Promise<PaginatedExperimentSavedMetricListApi> => {
return apiMutator<PaginatedExperimentSavedMetricListApi>(getExperimentSavedMetricsListUrl(projectId, params), {
...options,
method: 'GET',
})
}
export const getExperimentSavedMetricsCreateUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiment_saved_metrics/`
}
export const experimentSavedMetricsCreate = async (
projectId: string,
experimentSavedMetricApi: NonReadonly<ExperimentSavedMetricApi>,
options?: RequestInit
): Promise<ExperimentSavedMetricApi> => {
return apiMutator<ExperimentSavedMetricApi>(getExperimentSavedMetricsCreateUrl(projectId), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentSavedMetricApi),
})
}
export const getExperimentSavedMetricsRetrieveUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_saved_metrics/${id}/`
}
export const experimentSavedMetricsRetrieve = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentSavedMetricApi> => {
return apiMutator<ExperimentSavedMetricApi>(getExperimentSavedMetricsRetrieveUrl(projectId, id), {
...options,
method: 'GET',
})
}
export const getExperimentSavedMetricsUpdateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_saved_metrics/${id}/`
}
export const experimentSavedMetricsUpdate = async (
projectId: string,
id: number,
experimentSavedMetricApi: NonReadonly<ExperimentSavedMetricApi>,
options?: RequestInit
): Promise<ExperimentSavedMetricApi> => {
return apiMutator<ExperimentSavedMetricApi>(getExperimentSavedMetricsUpdateUrl(projectId, id), {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentSavedMetricApi),
})
}
export const getExperimentSavedMetricsPartialUpdateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_saved_metrics/${id}/`
}
export const experimentSavedMetricsPartialUpdate = async (
projectId: string,
id: number,
patchedExperimentSavedMetricApi?: NonReadonly<PatchedExperimentSavedMetricApi>,
options?: RequestInit
): Promise<ExperimentSavedMetricApi> => {
return apiMutator<ExperimentSavedMetricApi>(getExperimentSavedMetricsPartialUpdateUrl(projectId, id), {
...options,
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(patchedExperimentSavedMetricApi),
})
}
export const getExperimentSavedMetricsDestroyUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiment_saved_metrics/${id}/`
}
export const experimentSavedMetricsDestroy = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<void> => {
return apiMutator<void>(getExperimentSavedMetricsDestroyUrl(projectId, id), {
...options,
method: 'DELETE',
})
}
export const getExperimentsListUrl = (projectId: string, params?: ExperimentsListParams) => {
const normalizedParams = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : String(value))
}
})
const stringifiedParams = normalizedParams.toString()
return stringifiedParams.length > 0
? `/v1/projects/${projectId}/experiments/?${stringifiedParams}`
: `/v1/projects/${projectId}/experiments/`
}
/**
* List experiments for the current project. Supports filtering by status and archival state.
*/
export const experimentsList = async (
projectId: string,
params?: ExperimentsListParams,
options?: RequestInit
): Promise<PaginatedExperimentBasicListApi> => {
return apiMutator<PaginatedExperimentBasicListApi>(getExperimentsListUrl(projectId, params), {
...options,
method: 'GET',
})
}
export const getExperimentsCreateUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiments/`
}
/**
* Create a new experiment in draft status with optional metrics.
*/
export const experimentsCreate = async (
projectId: string,
experimentWriteApi: NonReadonly<ExperimentWriteApi>,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsCreateUrl(projectId), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentWriteApi),
})
}
export const getExperimentsRetrieveUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/`
}
/**
* Retrieve a single experiment by ID, including its current status, metrics, feature flag, and results metadata.
*/
export const experimentsRetrieve = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsRetrieveUrl(projectId, id), {
...options,
method: 'GET',
})
}
export const getExperimentsUpdateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsUpdate = async (
projectId: string,
id: number,
experimentWriteApi: NonReadonly<ExperimentWriteApi>,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsUpdateUrl(projectId, id), {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentWriteApi),
})
}
export const getExperimentsPartialUpdateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/`
}
/**
* Update an experiment. Use this to modify experiment properties such as name, description, metrics, variants, and configuration. Metrics can be added, changed and removed at any time. Feature-flag config (variants, rollout, payloads) is sent via the feature_flag object.
*/
export const experimentsPartialUpdate = async (
projectId: string,
id: number,
patchedExperimentWriteApi?: NonReadonly<PatchedExperimentWriteApi>,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsPartialUpdateUrl(projectId, id), {
...options,
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(patchedExperimentWriteApi),
})
}
export const getExperimentsDestroyUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/`
}
/**
* Hard delete of this model is not allowed. Use a patch API call to set "deleted" to true
*/
export const experimentsDestroy = async (projectId: string, id: number, options?: RequestInit): Promise<unknown> => {
return apiMutator<unknown>(getExperimentsDestroyUrl(projectId, id), {
...options,
method: 'DELETE',
})
}
export const getExperimentsActivityRetrieveUrl = (
projectId: string,
id: number,
params?: ExperimentsActivityRetrieveParams
) => {
const normalizedParams = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : String(value))
}
})
const stringifiedParams = normalizedParams.toString()
return stringifiedParams.length > 0
? `/v1/projects/${projectId}/experiments/${id}/activity/?${stringifiedParams}`
: `/v1/projects/${projectId}/experiments/${id}/activity/`
}
/**
* Change history for this experiment.
*
* Returns a paginated audit trail of changes to the experiment and its holdouts
* and shared metrics: who made each change, what changed (field-level before/after
* values), and when. Ordered newest first.
*/
export const experimentsActivityRetrieve = async (
projectId: string,
id: number,
params?: ExperimentsActivityRetrieveParams,
options?: RequestInit
): Promise<ActivityLogPaginatedResponseApi> => {
return apiMutator<ActivityLogPaginatedResponseApi>(getExperimentsActivityRetrieveUrl(projectId, id, params), {
...options,
method: 'GET',
})
}
export const getExperimentsArchiveCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/archive/`
}
/**
* Archive an ended experiment.
*
* Hides the experiment from the default list view. The experiment can be
* restored at any time by updating archived=false. When the linked feature
* flag is still enabled, pass disable_feature_flag=true to also disable and
* archive it. Returns 400 if the experiment is already archived or has not
* ended yet.
*/
export const experimentsArchiveCreate = async (
projectId: string,
id: number,
archiveExperimentApi?: ArchiveExperimentApi,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsArchiveCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(archiveExperimentApi),
})
}
export const getExperimentsCopyToProjectCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/copy_to_project/`
}
/**
* Copy an experiment into another project in the same organization as a new draft.
*/
export const experimentsCopyToProjectCreate = async (
projectId: string,
id: number,
copyExperimentToProjectApi: CopyExperimentToProjectApi,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsCopyToProjectCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(copyExperimentToProjectApi),
})
}
export const getExperimentsCreateExposureCohortForExperimentCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/create_exposure_cohort_for_experiment/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsCreateExposureCohortForExperimentCreate = async (
projectId: string,
id: number,
experimentApi: NonReadonly<ExperimentApi>,
options?: RequestInit
): Promise<void> => {
return apiMutator<void>(getExperimentsCreateExposureCohortForExperimentCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentApi),
})
}
export const getExperimentsDuplicateCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/duplicate/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsDuplicateCreate = async (
projectId: string,
id: number,
experimentApi: NonReadonly<ExperimentApi>,
options?: RequestInit
): Promise<void> => {
return apiMutator<void>(getExperimentsDuplicateCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentApi),
})
}
export const getExperimentsEndCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/end/`
}
/**
* End a running experiment without shipping a variant.
*
* Sets end_date to now and marks the experiment as stopped. The feature
* flag is NOT modified — users continue to see their assigned variants
* and exposure events ($feature_flag_called) continue to be recorded.
* However, only data up to end_date is included in experiment results.
*
* Use this when:
*
* - You want to freeze the results window without changing which variant
* users see.
* - A variant was already shipped manually via the feature flag UI and
* the experiment just needs to be marked complete.
*
* The end_date can be adjusted after ending via PATCH if it needs to be
* backdated (e.g. to match when the flag was actually paused).
*
* Other options:
* - Use ship_variant to end the experiment AND roll out a single variant to 100%% of users.
* - Use pause to deactivate the flag without ending the experiment (stops variant assignment but does not freeze results).
*
* Returns 400 if the experiment is not running.
*/
export const experimentsEndCreate = async (
projectId: string,
id: number,
endExperimentApi?: EndExperimentApi,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsEndCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(endExperimentApi),
})
}
export const getExperimentsFlagCleanupTargetRetrieveUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/flag_cleanup_target/`
}
/**
* Repository a flag-cleanup pull request for this experiment would be opened in.
*
* Resolution order: the experiment's saved repository, else the environment's default
* cleanup repository, else the team's only connected GitHub repository. When the team
* has several repositories and none is saved (source=ambiguous), pass one via
* `repository` on end/ship_variant.
*/
export const experimentsFlagCleanupTargetRetrieve = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentFlagCleanupTargetApi> => {
return apiMutator<ExperimentFlagCleanupTargetApi>(getExperimentsFlagCleanupTargetRetrieveUrl(projectId, id), {
...options,
method: 'GET',
})
}
export const getExperimentsFlagCleanupTaskRetrieveUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/flag_cleanup_task/`
}
/**
* Status of the flag-cleanup Desktop task opened for this experiment.
*
* When an experiment was ended or shipped with open_cleanup_pr=true, a Desktop task
* removes the experiment's feature-flag code and opens a draft pull request. This
* returns that task's latest run status and the PR URL once one is opened. Poll
* until is_terminal is true. Returns 404 when no cleanup task was opened.
*/
export const experimentsFlagCleanupTaskRetrieve = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentFlagCleanupTaskApi> => {
return apiMutator<ExperimentFlagCleanupTaskApi>(getExperimentsFlagCleanupTaskRetrieveUrl(projectId, id), {
...options,
method: 'GET',
})
}
export const getExperimentsFreezeExposureCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/freeze_exposure/`
}
/**
* Freeze exposure on a running experiment while metrics keep flowing.
*
* Snapshots the already-exposed users into a static cohort and narrows the
* linked feature flag so only those users keep matching — new users can no
* longer enter the experiment. ``end_date`` is left null so long-term metrics
* (revenue/LTV/renewals/retention) keep accumulating. Enrolled users keep
* their assigned variant. The serialized status becomes 'exposure_frozen'.
*
* Returns 400 if the experiment is not running, exposure is already frozen,
* the experiment is group-aggregated (group flags cannot be frozen with a
* person cohort), or the exposed set is too large to snapshot synchronously.
*/
export const experimentsFreezeExposureCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsFreezeExposureCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsLaunchCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/launch/`
}
/**
* Launch a draft experiment.
*
* Validates the experiment is in draft state, activates its linked feature flag,
* sets start_date to the current server time, and transitions the experiment to running.
* Returns 400 if the experiment has already been launched or if the feature flag
* configuration is invalid (e.g. fewer than 2 variants).
*/
export const experimentsLaunchCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsLaunchCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsMetricsRecalculationCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/metrics_recalculation/`
}
/**
* Trigger a batch recalculation of all metrics for this experiment.
*
* Returns 201 with the new pending recalculation, or 200 with the active one if a recalculation is
* already pending or in progress for this experiment. The response payload intentionally does not
* include the `results` array — at POST time the workflow has just been queued and no per-metric
* results exist yet. Clients should poll `GET metrics_recalculation/{id}/` for results as the workflow
* progresses.
*/
export const experimentsMetricsRecalculationCreate = async (
projectId: string,
id: number,
recalculateMetricsRequestApi?: RecalculateMetricsRequestApi,
options?: RequestInit
): Promise<ExperimentMetricsRecalculationApi> => {
return apiMutator<ExperimentMetricsRecalculationApi>(getExperimentsMetricsRecalculationCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(recalculateMetricsRequestApi),
})
}
export const getExperimentsMetricsRecalculationRetrieveUrl = (
projectId: string,
id: number,
recalculationId: string
) => {
return `/v1/projects/${projectId}/experiments/${id}/metrics_recalculation/${recalculationId}/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsMetricsRecalculationRetrieve = async (
projectId: string,
id: number,
recalculationId: string,
options?: RequestInit
): Promise<ExperimentMetricsRecalculationApi> => {
return apiMutator<ExperimentMetricsRecalculationApi>(
getExperimentsMetricsRecalculationRetrieveUrl(projectId, id, recalculationId),
{
...options,
method: 'GET',
}
)
}
export const getExperimentsMetricsRecalculationLatestRetrieveUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/metrics_recalculation/latest/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsMetricsRecalculationLatestRetrieve = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentMetricsRecalculationApi> => {
return apiMutator<ExperimentMetricsRecalculationApi>(
getExperimentsMetricsRecalculationLatestRetrieveUrl(projectId, id),
{
...options,
method: 'GET',
}
)
}
export const getExperimentsPauseCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/pause/`
}
/**
* Pause a running experiment.
*
* Deactivates the linked feature flag so it is no longer returned by the
* /decide endpoint. Users fall back to the application default (typically
* the control experience), and no new exposure events are recorded (i.e.
* $feature_flag_called is not fired).
* Returns 400 if the experiment is not running or is already paused.
*/
export const experimentsPauseCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsPauseCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsRecalculateTimeseriesCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/recalculate_timeseries/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsRecalculateTimeseriesCreate = async (
projectId: string,
id: number,
experimentApi: NonReadonly<ExperimentApi>,
options?: RequestInit
): Promise<void> => {
return apiMutator<void>(getExperimentsRecalculateTimeseriesCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentApi),
})
}
export const getExperimentsResetCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/reset/`
}
/**
* Reset an experiment back to draft state.
*
* Clears start/end dates, conclusion, archived flag, and any flag-cleanup
* task pointer. The feature flag is left unchanged — users continue to see
* their assigned variants.
*
* Previously collected events still exist but won't be included in
* results unless the start date is manually adjusted after re-launch.
*
* Returns 400 if the experiment is already in draft state.
*/
export const experimentsResetCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsResetCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsResumeCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/resume/`
}
/**
* Resume a paused experiment.
*
* Reactivates the linked feature flag so it is returned by /decide again.
* Users are re-bucketed deterministically into the same variants they had
* before the pause, and exposure tracking resumes.
* Returns 400 if the experiment is not running or is not paused.
*/
export const experimentsResumeCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsResumeCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsSessionBucketsCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/session_buckets/`
}
/**
* Session recordings of this experiment matching a bucket.
*
* Answers the questions a recordings query can't express on its own — "fired any of these
* metrics", "fired none of them", "was exposed but never completed the funnel in this
* session" — by returning a bounded, most-recent-first list of session IDs to pass back as
* a recordings query's session_ids. POST because the metric list doesn't fit a query
* string; the endpoint only reads.
*
* Session-scoped and goal-free: the set describes what happened in each session, while the
* experiment analysis counts per person over the whole run window. A session can be in the
* drop-off bucket while the same person converts in a later one.
*/
export const experimentsSessionBucketsCreate = async (
projectId: string,
id: number,
experimentSessionBucketRequestApi: ExperimentSessionBucketRequestApi,
options?: RequestInit
): Promise<ExperimentSessionBucketResponseApi> => {
return apiMutator<ExperimentSessionBucketResponseApi>(getExperimentsSessionBucketsCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentSessionBucketRequestApi),
})
}
export const getExperimentsShipVariantCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/ship_variant/`
}
/**
* Ship a variant and (optionally) end the experiment.
*
* Updates the feature flag so the selected variant gets 100% of the variant
* distribution. By default, existing release conditions on the flag are preserved
* untouched — the variant is served only to users who already match them. Pass
* ``release_to_everyone: true`` to also prepend a catch-all release condition
* that rolls the variant out to 100% of users (overrides any existing release
* conditions on the flag).
*
* Can be called on both running and stopped experiments. If the experiment is
* still running, it will also be ended (end_date set and status marked as stopped).
* If the experiment has already ended, only the flag is rewritten - this supports
* the "end first, ship later" workflow.
*
* If an approval policy requires review before changes on the flag take effect,
* the API returns 409 with a change_request_id. The experiment is NOT ended until
* the change request is approved and the user retries.
*
* Returns 400 if the experiment is in draft state, the variant_key is not found
* on the flag, or the experiment has no linked feature flag.
*/
export const experimentsShipVariantCreate = async (
projectId: string,
id: number,
shipVariantApi: ShipVariantApi,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsShipVariantCreateUrl(projectId, id), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(shipVariantApi),
})
}
export const getExperimentsTimeseriesResultsRetrieveUrl = (
projectId: string,
id: number,
params: ExperimentsTimeseriesResultsRetrieveParams
) => {
const normalizedParams = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : String(value))
}
})
const stringifiedParams = normalizedParams.toString()
return stringifiedParams.length > 0
? `/v1/projects/${projectId}/experiments/${id}/timeseries_results/?${stringifiedParams}`
: `/v1/projects/${projectId}/experiments/${id}/timeseries_results/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsTimeseriesResultsRetrieve = async (
projectId: string,
id: number,
params: ExperimentsTimeseriesResultsRetrieveParams,
options?: RequestInit
): Promise<void> => {
return apiMutator<void>(getExperimentsTimeseriesResultsRetrieveUrl(projectId, id, params), {
...options,
method: 'GET',
})
}
export const getExperimentsUnarchiveCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/unarchive/`
}
/**
* Unarchive an archived experiment.
*
* Restores the experiment to the default list view. Returns 400 if the
* experiment is not currently archived.
*/
export const experimentsUnarchiveCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsUnarchiveCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsUnfreezeExposureCreateUrl = (projectId: string, id: number) => {
return `/v1/projects/${projectId}/experiments/${id}/unfreeze_exposure/`
}
/**
* Reopen enrollment on an exposure-frozen experiment.
*
* Removes the snapshot-cohort condition and freeze markers from every release
* group, restoring the flag's original targeting: new users can enroll again
* and already-enrolled users keep their assigned variant. The snapshot cohort
* is soft-deleted. The serialized status returns to 'running'.
*
* Returns 400 if the experiment is not running or its exposure is not frozen.
*/
export const experimentsUnfreezeExposureCreate = async (
projectId: string,
id: number,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsUnfreezeExposureCreateUrl(projectId, id), {
...options,
method: 'POST',
})
}
export const getExperimentsCalculateRunningTimeCreateUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiments/calculate_running_time/`
}
/**
* Estimate the recommended sample size and running time for an experiment.
*
* Pure statistical calculation — does not read or write any experiment. Pass the metric type, a
* minimum detectable effect, and either a baseline value or raw baseline statistics. When
* `exposure_rate_per_day` is provided, the response also includes the estimated running time in days.
*/
export const experimentsCalculateRunningTimeCreate = async (
projectId: string,
runningTimeCalculationInputApi: RunningTimeCalculationInputApi,
options?: RequestInit
): Promise<RunningTimeCalculationResultApi> => {
return apiMutator<RunningTimeCalculationResultApi>(getExperimentsCalculateRunningTimeCreateUrl(projectId), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(runningTimeCalculationInputApi),
})
}
export const getExperimentsCreateFromPromptCreateUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiments/create_from_prompt/`
}
/**
* Create an experiment that compares N versions of an LLM prompt using a metric template.
*
* The user picks 2+ versions of an existing LLMPrompt and 1+ metric templates
* (cost / latency / eval_pass_rate). The endpoint builds the matching variants
* (control + test-N, each named after its prompt version) and attaches one
* metric per selected template, each scoped to the prompt's $ai_prompt_name.
* Resulting experiment is in draft state.
*/
export const experimentsCreateFromPromptCreate = async (
projectId: string,
createFromPromptInputApi: CreateFromPromptInputApi,
options?: RequestInit
): Promise<ExperimentApi> => {
return apiMutator<ExperimentApi>(getExperimentsCreateFromPromptCreateUrl(projectId), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(createFromPromptInputApi),
})
}
export const getExperimentsPromptTemplatesRetrieveUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiments/prompt_templates/`
}
/**
* List the LLM metric templates that can be passed to `create_from_prompt`.
*/
export const experimentsPromptTemplatesRetrieve = async (
projectId: string,
options?: RequestInit
): Promise<ExperimentsPromptTemplatesRetrieve200Item[]> => {
return apiMutator<ExperimentsPromptTemplatesRetrieve200Item[]>(
getExperimentsPromptTemplatesRetrieveUrl(projectId),
{
...options,
method: 'GET',
}
)
}
export const getExperimentsSessionContextRetrieveUrl = (
projectId: string,
params: ExperimentsSessionContextRetrieveParams
) => {
const normalizedParams = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : String(value))
}
})
const stringifiedParams = normalizedParams.toString()
return stringifiedParams.length > 0
? `/v1/projects/${projectId}/experiments/session_context/?${stringifiedParams}`
: `/v1/projects/${projectId}/experiments/session_context/`
}
/**
* Resolve which experiments (and variants) a session recording saw. Variants come from the session's $feature_flag_called events and stamped $feature/<key> event properties — flag evaluation, which may differ from an experiment's exposure criteria.
*/
export const experimentsSessionContextRetrieve = async (
projectId: string,
params: ExperimentsSessionContextRetrieveParams,
options?: RequestInit
): Promise<ExperimentSessionContextResponseApi> => {
return apiMutator<ExperimentSessionContextResponseApi>(getExperimentsSessionContextRetrieveUrl(projectId, params), {
...options,
method: 'GET',
})
}
export const getExperimentsSessionContextsCreateUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiments/session_contexts/`
}
/**
* Resolve experiment context for a batch of session recordings.
*
* Batch variant of `session_context`, used to prefetch the replay player's experiments
* box for a whole recordings list in one request. POST because the id list doesn't fit a
* query string; the endpoint only reads. Already-computed sessions are served from (and
* cold ones written to) the same short-lived per-viewer cache the single-session endpoint
* uses, so opening any prefetched recording renders its context instantly. Sessions whose
* recording metadata doesn't exist yet are omitted from the response, as are recordings
* the caller can't access and sessions beyond the batch's recording-day budget (each
* distinct recording day costs its own set of Datastore scans, so only the most recent
* days are computed per request).
*/
export const experimentsSessionContextsCreate = async (
projectId: string,
experimentSessionContextsRequestApi: ExperimentSessionContextsRequestApi,
options?: RequestInit
): Promise<ExperimentSessionContextsResponseApi> => {
return apiMutator<ExperimentSessionContextsResponseApi>(getExperimentsSessionContextsCreateUrl(projectId), {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(experimentSessionContextsRequestApi),
})
}
export const getExperimentsStatsRetrieveUrl = (projectId: string) => {
return `/v1/projects/${projectId}/experiments/stats/`
}
/**
* Mixin for ViewSets to handle approval-gate exceptions raised from decorated serializers.
*
* Intercepts ApprovalRequired (409) and PolicyConflict (400) raised by the @approval_gate
* decorator on serializer methods and converts them into the same responses the viewset path
* produces (see decorators._result_to_response), so both paths share one contract.
*/
export const experimentsStatsRetrieve = async (projectId: string, options?: RequestInit): Promise<void> => {
return apiMutator<void>(getExperimentsStatsRetrieveUrl(projectId), {
...options,
method: 'GET',
})
}