Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.12.0"
".": "0.13.0"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 29
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/formbricks/hub-876b3cc61d59eee7f50b21239e174146afb6176473af1f7c6ac6ce8900e40b9c.yml
openapi_spec_hash: f0acdda9bda9e4733d298cf9082b58e7
config_hash: 0b48961f5f0b4dca3a9a4342cd42bd51
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/formbricks/hub-0822f44f62ac4c826260c8072710c5c7f490653c2c9c9666e349d34cc9c99e90.yml
openapi_spec_hash: 4eeeec7b5b6a4d484eec92d1718dc640
config_hash: a4efd21029f0fb9bf9e9abd69b06ac84
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.13.0 (2026-08-25)

Full Changelog: [v0.12.0...v0.13.0](https://github.com/formbricks/hub-typescript/compare/v0.12.0...v0.13.0)

### Features

* add a records-scoped tenant purge endpoint (ENG-2129) ([a6b9afa](https://github.com/formbricks/hub-typescript/commit/a6b9afa33356b7f582c11c0f56d6d242153ee0da))
* report why an enrichment is off and what failed, durably (ENG-2375) ([b6c363b](https://github.com/formbricks/hub-typescript/commit/b6c363bf4bc67068b1d7f32892fde0da179750c9))


### Bug Fixes

* **taxonomy:** harden run persistence ([721e58b](https://github.com/formbricks/hub-typescript/commit/721e58b7c66a7800040b6b31e277f5f2191f766b))

## 0.12.0 (2026-08-12)

Full Changelog: [v0.11.0...v0.12.0](https://github.com/formbricks/hub-typescript/compare/v0.11.0...v0.12.0)
Expand Down
2 changes: 2 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ Methods:
Types:

- <code><a href="./src/resources/tenants/tenants.ts">TenantDeleteDataResponse</a></code>
- <code><a href="./src/resources/tenants/tenants.ts">TenantPurgeFeedbackRecordsResponse</a></code>

Methods:

- <code title="delete /v1/tenants/{tenant_id}/data">client.tenants.<a href="./src/resources/tenants/tenants.ts">deleteData</a>(tenantID) -> TenantDeleteDataResponse</code>
- <code title="delete /v1/tenants/{tenant_id}/feedback-records">client.tenants.<a href="./src/resources/tenants/tenants.ts">purgeFeedbackRecords</a>(tenantID) -> TenantPurgeFeedbackRecordsResponse</code>

## Settings

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@formbricks/hub",
"version": "0.12.0",
"version": "0.13.0",
"description": "The official TypeScript library for the Formbricks Hub API",
"author": "Formbricks Hub <hola@formbricks.com>",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "@formbricks/hub-mcp",
"version": "0.12.0",
"version": "0.13.0",
"description": "The official MCP Server for the Formbricks Hub API",
"author": {
"name": "Formbricks Hub",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@formbricks/hub-mcp",
"version": "0.12.0",
"version": "0.13.0",
"description": "The official MCP Server for the Formbricks Hub API",
"author": "Formbricks Hub <hola@formbricks.com>",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-server/src/code-tool-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const fuse = new Fuse(
'client.webhooks.retrieve',
'client.webhooks.update',
'client.tenants.deleteData',
'client.tenants.purgeFeedbackRecords',
'client.tenants.settings.retrieve',
'client.tenants.settings.update',
'client.taxonomy.listFields',
Expand Down
35 changes: 30 additions & 5 deletions packages/mcp-server/src/local-docs-search.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/mcp-server/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ export const sdkMethods: SdkMethod[] = [
httpMethod: 'delete',
httpPath: '/v1/tenants/{tenant_id}/data',
},
{
clientCallName: 'client.tenants.purgeFeedbackRecords',
fullyQualifiedName: 'tenants.purgeFeedbackRecords',
httpMethod: 'delete',
httpPath: '/v1/tenants/{tenant_id}/feedback-records',
},
{
clientCallName: 'client.tenants.settings.retrieve',
fullyQualifiedName: 'tenants.settings.retrieve',
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const newMcpServer = async ({
new McpServer(
{
name: 'formbricks_hub_api',
version: '0.12.0',
version: '0.13.0',
},
{
instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }),
Expand Down
12 changes: 10 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ import {
TaxonomyListFieldsParams,
TaxonomyListFieldsResponse,
} from './resources/taxonomy/taxonomy';
import { TenantDeleteDataResponse, Tenants } from './resources/tenants/tenants';
import {
TenantDeleteDataResponse,
TenantPurgeFeedbackRecordsResponse,
Tenants,
} from './resources/tenants/tenants';
import { type Fetch } from './internal/builtin-types';
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
import { FinalRequestOptions, RequestOptions } from './internal/request-options';
Expand Down Expand Up @@ -837,7 +841,11 @@ export declare namespace FormbricksHub {
type WebhookListParams as WebhookListParams,
};

export { Tenants as Tenants, type TenantDeleteDataResponse as TenantDeleteDataResponse };
export {
Tenants as Tenants,
type TenantDeleteDataResponse as TenantDeleteDataResponse,
type TenantPurgeFeedbackRecordsResponse as TenantPurgeFeedbackRecordsResponse,
};

export {
Taxonomy as Taxonomy,
Expand Down
44 changes: 41 additions & 3 deletions src/resources/enrichment-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ export class EnrichmentStatus extends APIResource {
* enrichment is active for the tenant (deployment-configured and switched on /
* with a resolvable target language), and `eligible`/`done` are directory-level
* counts of feedback records that qualify and that have been enriched — the UI
* derives "in progress" as `eligible - done`. When an enrichment is not enabled
* its counts are zero. The response contains counts only (no record identifiers or
* content).
* derives "in progress" as `eligible - done`, of which `failed` is retryable and
* `failed_terminal` never will be. When an enrichment is not enabled its counts
* are zero and `disabled_reason` names the gate that closed it, so the UI can
* explain _why_ rather than silently hiding the enrichment. `as_of` is when the
* counts were computed, which lets a polling client derive throughput and an ETA
* without the Hub computing either. The response contains counts only (no record
* identifiers or content).
*/
retrieve(
query: EnrichmentStatusRetrieveParams,
Expand Down Expand Up @@ -46,13 +50,47 @@ export interface TypeStatus {
* switched on / with a resolvable target language).
*/
enabled: boolean;

/**
* Eligible records whose last enrichment attempt gave up but which a retry could
* still rescue — a provider outage, a timeout. Counted only while the record is
* still un-enriched, so a later success removes it without any cleanup.
*/
failed: number;

/**
* Eligible records the provider will never accept, because the outcome is a
* property of the record's own text — a content-policy block, a refusal, an input
* past the model's limit. Retrying these cannot help; they resolve only if the
* text changes.
*/
failed_terminal: number;

/**
* Which gate switched the enrichment off. Present exactly when `enabled` is false,
* and absent otherwise. `not_configured` — the deployment has no provider/model
* for this enrichment, so it is off for every tenant and only an operator can fix
* it. `switched_off` — the tenant turned it off (sentiment and emotions only).
* `no_target_language` — translation is configured but neither the tenant's
* `target_language` nor the deployment default resolves (translation only; it has
* no on/off switch, so an absent target is its off state).
*/
disabled_reason?: 'not_configured' | 'switched_off' | 'no_target_language';
}

/**
* A tenant's enrichment progress across the record-level enrichments. Counts are
* directory-level totals.
*/
export interface EnrichmentStatusRetrieveResponse {
/**
* When the counts were computed (UTC), not when the response was serialized. The
* endpoint is polled, so two responses are enough to derive throughput and an ETA
* client-side from the change in `done` over the change in `as_of` — the Hub
* computes neither.
*/
as_of: string;

/**
* One enrichment's progress for a tenant. When `enabled` is false, `eligible` and
* `done` are zero.
Expand Down
6 changes: 5 additions & 1 deletion src/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export {
type TaxonomyListFieldsResponse,
type TaxonomyListFieldsParams,
} from './taxonomy/taxonomy';
export { Tenants, type TenantDeleteDataResponse } from './tenants/tenants';
export {
Tenants,
type TenantDeleteDataResponse,
type TenantPurgeFeedbackRecordsResponse,
} from './tenants/tenants';
export {
Webhooks,
type WebhookCreateResponse,
Expand Down
2 changes: 1 addition & 1 deletion src/resources/tenants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export {
type SettingUpdateResponse,
type SettingUpdateParams,
} from './settings';
export { Tenants, type TenantDeleteDataResponse } from './tenants';
export { Tenants, type TenantDeleteDataResponse, type TenantPurgeFeedbackRecordsResponse } from './tenants';
99 changes: 98 additions & 1 deletion src/resources/tenants/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,76 @@ export class Tenants extends APIResource {
deleteData(tenantID: string, options?: RequestOptions): APIPromise<TenantDeleteDataResponse> {
return this._client.delete(path`/v1/tenants/${tenantID}/data`, options);
}

/**
* Permanently deletes every feedback record for the specified tenant_id,
* everything derived from those records — embeddings and the enrichment stored on
* each record (sentiment, emotions, translations) — and the taxonomy built on
* them: runs, clusters, nodes, cluster memberships, active-run pointers and node
* events.
*
* This is intended for emptying a dataset that stays in use, so it removes the
* tenant's DATA but never its CONFIGURATION: webhooks and tenant settings are left
* untouched. That is the difference from `DELETE /v1/tenants/{tenant_id}/data`,
* which additionally deletes both and is meant for offboarding a deprovisioned
* tenant.
*
* The taxonomy is removed rather than preserved because it describes records that
* no longer exist: its per-node counts are derived from memberships and would all
* read zero, while a run's own stored counters (`record_count`, `cluster_count`, a
* cluster's `size`) are historical and would keep advertising the old numbers. A
* new taxonomy can be generated once the dataset has enough feedback again; manual
* node renames and removals are per-run and do not survive a regeneration in any
* case.
*
* The tenant is a required path segment, so it cannot be omitted the way a filter
* on a collection delete could be — dropping it routes elsewhere rather than
* widening the operation to every tenant.
*
* Asynchronous: the request schedules the purge and returns 202 immediately,
* because the deletion is unbounded and can outlive a request. There is therefore
* no deleted count in the response. Poll
* `GET /v1/feedback-records/count?tenant_id=...` to observe progress.
*
* The purge removes only the records that existed when it started — it takes a
* high-water mark up front — so feedback ingested while it runs is never deleted.
* For a dataset that is no longer receiving feedback the count reaching zero means
* the purge is complete; for one still ingesting, a nonzero count is those newer
* records. Note this means the count alone cannot distinguish "finished" from
* "failed" on an active dataset.
*
* Idempotent and safe to repeat. Requesting a purge while one is already running
* for the same tenant joins the running purge rather than queueing a second one,
* and still returns 202. A purge requested after an earlier one finished starts a
* new run.
*
* Records are deleted in committed batches, so a purge interrupted by a restart or
* a timeout keeps the progress it made and resumes on retry; the taxonomy is
* removed in a final step once the records are gone. While each step runs, the
* tenant's write lock is held exclusively and Hub-owned writes for that tenant are
* rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released
* between steps, and writes for other tenants are never affected. This does not
* apply to the request below — scheduling a purge takes no lock, so this endpoint
* does not return 409. A batch that cannot acquire the lock is retried by the job
* queue without any caller action, up to a bounded number of attempts.
*
* No webhook events are published for a purge, and no webhooks are deleted.
* Enrichment jobs already queued for purged records no-op when they run, since the
* record they reference is gone.
*
* @example
* ```ts
* const response = await client.tenants.purgeFeedbackRecords(
* 'org-123',
* );
* ```
*/
purgeFeedbackRecords(
tenantID: string,
options?: RequestOptions,
): APIPromise<TenantPurgeFeedbackRecordsResponse> {
return this._client.delete(path`/v1/tenants/${tenantID}/feedback-records`, options);
}
}

export interface TenantDeleteDataResponse {
Expand Down Expand Up @@ -79,6 +149,11 @@ export interface TenantDeleteDataResponse {
*/
deleted_taxonomy_nodes: number;

/**
* Number of immutable taxonomy run-input snapshot rows deleted
*/
deleted_taxonomy_run_input_records: number;

/**
* Number of taxonomy runs deleted
*/
Expand All @@ -100,10 +175,32 @@ export interface TenantDeleteDataResponse {
tenant_id: string;
}

export interface TenantPurgeFeedbackRecordsResponse {
/**
* Always `accepted`. The purge runs in the background, so this reports that the
* work was scheduled, not that it finished — poll `GET /v1/feedback-records/count`
* for the tenant to observe completion.
*/
status: 'accepted';

/**
* Tenant ID whose feedback records are being purged
*/
tenant_id: string;

/**
* Human-readable confirmation
*/
message?: string;
}

Tenants.Settings = Settings;

export declare namespace Tenants {
export { type TenantDeleteDataResponse as TenantDeleteDataResponse };
export {
type TenantDeleteDataResponse as TenantDeleteDataResponse,
type TenantPurgeFeedbackRecordsResponse as TenantPurgeFeedbackRecordsResponse,
};

export {
Settings as Settings,
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '0.12.0'; // x-release-please-version
export const VERSION = '0.13.0'; // x-release-please-version
12 changes: 12 additions & 0 deletions tests/api-resources/tenants/tenants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,16 @@ describe('resource tenants', () => {
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});

// Mock server tests are disabled
test.skip('purgeFeedbackRecords', async () => {
const responsePromise = client.tenants.purgeFeedbackRecords('org-123');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
Loading