diff --git a/src/docs.json b/src/docs.json index f0ed73c0..11d28639 100644 --- a/src/docs.json +++ b/src/docs.json @@ -316,6 +316,8 @@ { "group": "Troubleshooting guides", "pages": [ + "troubleshooting-guides/overview", + "troubleshooting-guides/general", "troubleshooting-guides/salesforce" ] }, diff --git a/src/generate-docs.ts b/src/generate-docs.ts index 592fa3af..6d8b1e7a 100644 --- a/src/generate-docs.ts +++ b/src/generate-docs.ts @@ -548,6 +548,8 @@ const baseConfig = { { group: "Troubleshooting guides", pages: [ + "troubleshooting-guides/overview", + "troubleshooting-guides/general", "troubleshooting-guides/salesforce" ] }, diff --git a/src/troubleshooting-guides/general.mdx b/src/troubleshooting-guides/general.mdx new file mode 100644 index 00000000..fbd67241 --- /dev/null +++ b/src/troubleshooting-guides/general.mdx @@ -0,0 +1,77 @@ +--- +title: "General" +description: "Fixes for errors that can occur with any provider: paused reads, rate limits, webhook delivery, credentials, and async writes." +--- + +These issues can occur with any provider. For provider-specific errors, see the guides in this section or use the [troubleshooting index](/troubleshooting-guides/overview). + +## Scheduled reads stopped running + +**Symptom**: reads for an installation no longer run on schedule, or you received a `read.schedule.paused` [notification](/notifications/overview). + +**Why this happens**: Ampersand pauses a read schedule when runs keep failing, most often because the customer's connection has expired or lost access. + +**How to fix**: + +1. Check the connection's status in the [Dashboard](https://dashboard.withampersand.com) or with the [Get connection endpoint](/reference/connection/get-a-connection). If it shows `bad_credentials`, have your customer [update the connection](/customer-guides/update-connection). +2. Once the connection is healthy, resume the schedule with the [Unpause reads endpoint](/reference/read/unpause-reads-for-an-installation). +3. Confirm the next run with the [List operations endpoint](/reference/operation/list-operations). + +## Proxy call returns `429 Too Many Requests` + +**Symptom**: [Proxy actions](/proxy-actions) intermittently return HTTP 429. + +**Why this happens**: the provider is rate limiting the customer's account. Many providers penalize repeated 429s in a row, and some lock out a token entirely after too many unsuccessful requests. + +**How to fix**: + +1. Send the `X-Amp-Rate-Limiter-Mode: throttle` request header. Ampersand will then stop forwarding requests to the provider until it's safe to retry. +2. On a 429 response, read the `X-Amp-Retry-After` header (a UTC timestamp for when to retry) and `X-Amp-Retryable` (whether retrying is possible at all). +3. Retry after the suggested time. See [managing API rate limits](/proxy-actions#use-ampersand-to-manage-api-rate-limits). + +## Webhook receiver rejects large payloads + +**Symptom**: webhook deliveries fail or your endpoint returns `413 Payload Too Large`, especially during backfills. + +**Why this happens**: Ampersand webhook payloads can be up to **300 KB**, and some HTTP frameworks default to a smaller request body limit. For example, Express's `json` middleware defaults to 100 KB. + +**How to fix**: + +1. Raise your framework's body size limit to at least 350 KB (for example, `express.json({ limit: "350kb" })`). +2. If your receiver can't accept payloads of this size at all, deliver results by URL instead. See [handling the payload size](/destinations/webhooks#handling-the-payload-size). + +## Webhook signature verification fails + +**Symptom**: the signature on incoming webhook messages doesn't validate. + +**Why this happens**: Ampersand delivers webhooks through Svix, and each destination has its own signing secret. Verifying with the wrong destination's secret, or reading the wrong header, fails validation. + +**How to fix**: + +1. Read the signature from the `svix-signature` header. +2. Verify it with the **webhook signing secret for that specific destination**, found in the [Dashboard](https://dashboard.withampersand.com). +3. Follow the steps in [webhook signature verification](/destinations/webhooks#webhook-signature-verification). + +## Connection shows `bad_credentials` + +**Symptom**: a connection's status is `bad_credentials`, or an installation stopped working after the customer changed their password, rotated a key, or revoked access. + +**Why this happens**: the credentials Ampersand holds for the customer's SaaS instance are no longer valid, so every API call on their behalf fails. + +**How to fix**: + +1. Have your customer re-authenticate from the **Manage** tab of the embedded UI. See [update a connection](/customer-guides/update-connection). +2. If they need to switch accounts entirely, the same flow applies. +3. If reads were paused in the meantime, [unpause them](/reference/read/unpause-reads-for-an-installation) after re-authentication. + +## Async write accepted but the record is missing + +**Symptom**: an asynchronous [write](/write-actions) returned successfully, but the record never appeared in the customer's SaaS. + +**Why this happens**: async mode accepts the write and processes it in the background. A success response means *accepted*, not *applied*. The operation may still be retrying or may have failed after retries. Ampersand retries failures with exponential backoff for 1 hour by default (configurable up to 48 hours). + +**How to fix**: + +1. Take the operation ID from the write response and check it with the [Get operation endpoint](/reference/operation/get-an-operation). +2. If the operation failed, inspect its logs with the [List logs endpoint](/reference/operation/list-logs-for-an-operation) for the provider's error. +3. For time-sensitive writes, consider [synchronous mode](/write-actions#synchronous-mode), or lengthen the [retry deadline](/write-actions#retry-policy-for-async-writes). diff --git a/src/troubleshooting-guides/overview.mdx b/src/troubleshooting-guides/overview.mdx new file mode 100644 index 00000000..403da1ce --- /dev/null +++ b/src/troubleshooting-guides/overview.mdx @@ -0,0 +1,183 @@ +--- +title: "Troubleshooting" +description: "Searchable index of known errors and their fixes, both general and provider-specific." +--- + +{/* + HOW TO ADD AN ENTRY + =================== + 1. Write the fix as a `##` section on the right page: + - cross-provider issues -> troubleshooting-guides/general.mdx + - provider-specific ones -> troubleshooting-guides/.mdx (create it if needed + and register it in generate-docs.ts) + Use this structure inside the section: + ## + **Symptom**: what the builder sees (error message verbatim if there is one). + **Why this happens**: the actual cause, one short paragraph. + **How to fix**: numbered steps, each one actionable, linking to the exact doc/API page. + 2. Add a row to the index below: title, one-line description, data-tags + (provider + areas + error keywords, space-separated, lowercase; used for search), + a visible area tag, and the href with the section's anchor. + 3. Run `pnpm run gen-docs` if you added a new page, then `make validate_links`. +*/} + +Known errors and how to fix them. Search by error message or symptom, or filter by provider. Every entry links to a step-by-step fix. + +
+
+ +
+ + + + +
+
+
+ +
+
Scheduled reads stopped running
+
Reads paused after repeated failures. Check the connection, then unpause via API.
+
+
read
+
+ +
+
Proxy call returns `429 Too Many Requests`
+
Use throttle mode and the retry-after headers instead of hammering the provider.
+
+
proxy
+
+ +
+
Webhook receiver rejects large payloads
+
Payloads reach 300 KB, so raise your body-size limit or switch to URL delivery.
+
+
webhooks
+
+ +
+
Webhook signature verification fails
+
Verify the `svix-signature` header with that destination's own signing secret.
+
+
webhooks
+
+ +
+
Connection shows `bad_credentials`
+
The customer's credentials are no longer valid, so have them re-authenticate.
+
+
auth
+
+ +
+
Async write accepted but the record is missing
+
Accepted is not applied. Check the operation's status and logs.
+
+
write
+
+ +
+
Salesforce read: `No such column` error
+
The connected user lacks field-level visibility, or the field name is wrong.
+
+
read
+
+ +
+
Salesforce write: `No such column` error
+
The field doesn't exist in the customer's org, or the user can't see it.
+
+
write
+
+ +
+
Salesforce subscribe fails with `LIMIT_EXCEEDED`
+
Salesforce allows Change Data Capture on 5 entities by default. Reduce objects or add the license.
+
+
subscribe
+
+ +
+
Salesforce subscribe events don't fire for a field
+
Salesforce only emits change events for direct fields, not derived ones like LastActivityDate.
+
+
subscribe
+
+ +
+
Salesforce connection turns `bad_credentials`
+
Missing refresh_token scope, a reused app, an expiring token policy, or revoked access.
+
+
auth
+
+ +
+
Salesforce returns `INVALID_SESSION_ID`
+
Check the connection's scopes, app reuse across projects, and duplicate connections.
+
+
auth
+
+ +
+
Salesforce read: `too many junction ids`
+
EmailMessage junction lookups exceed 500. Drop the ID fields or read EmailMessageRelation.
+
+
read
+
+ +
+
Salesforce OAuth: `OAUTH_APPROVAL_ERROR_GENERIC`
+
The installing user needs Use Any API Client or Approve Uninstalled Connected Apps.
+
+
auth
+
+ +
+
Salesforce OAuth: `OAUTH_EC_APP_NOT_FOUND`
+
The package was never installed, or the app was created in the wrong org.
+
+
auth
+
+ +
+
Salesforce: `Object type is not supported` for a standard object
+
Usually an object permission gap for the connected user, not a missing object.
+
+
read
+
+ +
+
Check a customer's Salesforce API quota
+
Query the limits endpoint through the proxy when you suspect rate limiting.
+
+
proxy
+
+ +
+
Salesforce (JWT) installation fails
+
Certificate, consumer key, and OAuth policy checks for the JWT flow.
+
+
auth
+
+ +
+
Google delegation: `unauthorized_client` error
+
Domain-wide delegation isn't authorized for the service account's client ID and scopes.
+
+
auth
+
+ +
+
Google delegation: `invalid_grant` error
+
The impersonated user or key is invalid. Check the connection details.
+
+
auth
+
+
+
+
+ +## Can't find your error? + +Email [support@withampersand.com](mailto:support@withampersand.com) or ask in your shared Slack channel with our engineers. If you think the docs should cover it, [file an issue](https://github.com/amp-labs/docs/issues). diff --git a/src/troubleshooting-guides/salesforce.mdx b/src/troubleshooting-guides/salesforce.mdx index 9e616f40..c0f7f3f2 100644 --- a/src/troubleshooting-guides/salesforce.mdx +++ b/src/troubleshooting-guides/salesforce.mdx @@ -32,7 +32,7 @@ Ampersand pauses reads automatically after repeated failures to avoid overloadin ### Verify the field name 1. Look up the field in the [Salesforce Object Reference](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_concepts.htm). If it's listed, the field is standard and this is a [field-level visibility issue](#resolve-a-field-level-visibility-issue). -2. If it's not listed, it's a custom field. Custom field API names must end with `__c` — if your read object configuration is missing the suffix, fix it. +2. If it's not listed, it's a custom field. Custom field API names must end with `__c`. If your read object configuration is missing the suffix, fix it. 3. If the suffix is already correct, this is a [field-level visibility issue](#resolve-a-field-level-visibility-issue). ## Write Action: `No such column` error @@ -59,8 +59,175 @@ Call [Get object metadata via installation](/reference/objects-&-fields/get-obje ### Field doesn't exist in the customer's org -Remove the field from your write payload for this customer — for example, by checking object metadata before writing, or by falling back to a default. Longer-term, update your application to handle per-customer schema differences. +Remove the field from your write payload for this customer, for example by checking object metadata before writing, or by falling back to a default. Longer-term, update your application to handle per-customer schema differences. ### Connected user lacks field-level visibility If the field exists in the org but isn't visible to the connected user's profile, Salesforce returns the same `No such column` error. Follow the steps in [Resolve a field-level visibility issue](#resolve-a-field-level-visibility-issue). + +## Subscribe installation fails with `LIMIT_EXCEEDED` + +Creating an installation with subscribe actions fails with an error similar to: + +```text +You can't select more entities. You've exceeded the maximum number of unique +Change Data Capture selected entities. Your total number of entities includes +the ones selected here and through custom channels. (errorCode: LIMIT_EXCEEDED) +``` + +By default, a Salesforce instance allows Change Data Capture on at most **five entities**. Entities your customer has selected through custom channels count toward the same limit, so the ceiling can be reached before your integration is installed. + +You have two ways to resolve this: + +1. Reduce the number of objects your integration subscribes to, so the total stays within the customer's limit. +2. Ask your customer to purchase the Salesforce Change Data Capture add-on license, which removes the limit on their instance. + +## Subscribe events do not fire when a field changes + +A subscription is healthy and other updates arrive, but changes to one specific field never produce an event. + +Salesforce only emits change events for **direct fields** on a record. Derived fields such as `LastActivityDate` never trigger change events, no matter how they are configured. See Salesforce's [change event documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_associated_objects_change_event.htm) for the full list of limitations. + +Watch a direct field instead, or subscribe to the object whose direct fields capture the change you care about. + +## Connection shows `bad_credentials` after working previously + +An integration that used to work starts failing: proxy requests return `401`, reads and writes fail, and the connection's status is `bad_credentials`. + + +**Which case is this?** +- Failing for *every* customer → most likely your [External Client App is misconfigured](#check-your-external-client-app) or shared across projects. +- Failing for *one* customer → most likely a [token policy or revoked access](#check-the-customers-salesforce-settings) on their side. + + +### Check your External Client App + +Confirm the app requests the `refresh_token` scope. Without it, Salesforce issues an access token that expires and cannot be renewed. Review your configuration against [Create External Client App](/provider-guides/salesforce#4-create-external-client-app). + +Also confirm you are not reusing one app across multiple Ampersand projects. Each project should have its own app. + +### Check the customer's Salesforce settings + +Two customer-side settings commonly invalidate credentials: + +1. **Refresh token policy.** If their policy expires refresh tokens after a set period, credentials break on that schedule. Ask them to set the policy to **Refresh token is valid until revoked** in the app's OAuth policies. +2. **Revoked access.** A Salesforce admin can revoke an app from the OAuth Connected Apps Usage page. Ask their admin to confirm the app has not been revoked. + +Once resolved, your customer can reconnect from the **Manage** tab. See [Update a connection](/customer-guides/update-connection). + +## Salesforce returns `INVALID_SESSION_ID` + +An API call fails with an error similar to: + +```json +{ + "message": "This session is not valid for use with the specified REST API endpoint. One of the following scopes is required: Full access, Manage user data via APIs, Access Connect REST API resources", + "errorCode": "INVALID_SESSION_ID" +} +``` + +Work through these in order: + +1. Confirm the connection's scopes include both `api` and `refresh_token`. If either is missing, correct your [External Client App configuration](/provider-guides/salesforce#4-create-external-client-app). +2. Confirm you are not using the same app across multiple projects. +3. Check for duplicate connections for the same customer. Connecting the same Salesforce instance twice invalidates the earlier refresh token, so the first connection stops working. Delete the duplicates. + +If none of those apply, ask your customer to uninstall and reinstall the integration, following the [Salesforce customer guide](/customer-guides/salesforce). + +## Read fails with `too many junction ids` + +A read of `EmailMessage` fails with: + +```text +Query is requesting too many junction ids; limit is 500 +``` + +The query exceeded Salesforce's limit on junction object lookups. On `EmailMessage`, the junction fields are `ToIds`, `CcIds`, `BccIds`, and `ContentDocumentIds`. A single mass email with hundreds of recipients can exceed the limit of 500 on its own. + + +**Which case is this?** Check whether you actually need the recipient IDs, or only their addresses. +- You only need addresses → [remove the junction fields](#remove-the-junction-fields). +- You need the IDs → [read `EmailMessageRelation` instead](#read-emailmessagerelation-instead). + + +### Remove the junction fields + +Drop the junction fields from `requiredFields` in your manifest: + +```yaml +requiredFields: + - fieldName: toids # remove this +``` + +The recipient addresses remain available through `toAddress`, `ccAddress`, and `bccAddress`, so most integrations lose nothing. + +### Read `EmailMessageRelation` instead + +If you need the IDs themselves, add the [`EmailMessageRelation`](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_emailmessagerelation.htm) object to your read action. This is the object that `ToIds`, `CcIds`, and `BccIds` actually refer to, and it exposes the same information without the junction limit: + +- `EmailMessageId` +- `RelationAddress` +- `RelationId` +- `RelationObjectType` +- `RelationType`, for example `ToAddress`, `CcAddress`, or `BccAddress` + +## `OAUTH_APPROVAL_ERROR_GENERIC` in the OAuth window + +Your customer sees `OAUTH_APPROVAL_ERROR_GENERIC` in the Salesforce OAuth window while installing the integration. + +This is a Salesforce permission issue: the installing user is not allowed to approve the app. Their admin needs to enable one of the following for that user: + +- **Use Any API Client**, if their org has API Access Control enabled +- **Approve Uninstalled Connected Apps**, if **Use Any API Client** is not shown + +Both are covered in [Required system permissions](/customer-guides/salesforce#required-system-permissions). + +## `OAUTH_EC_APP_NOT_FOUND` in the OAuth window + +The OAuth window reports that the external client app is not installed in the org. + + +**Which case is this?** +- Only this customer is affected → they likely [never installed the package](#the-customer-did-not-install-the-package). +- Every customer is affected → your app is likely [in the wrong org](#the-app-was-created-in-the-wrong-org). + + +### The customer did not install the package + +Your customer must open the package install URL and install the package before connecting. Send them the URL described in [Build the Package Install URL](/provider-guides/salesforce#6-build-the-package-install-url). + +### The app was created in the wrong org + +The External Client App must be created in your **Dev Hub Org**, not your Namespace Org. If it was created in the wrong org, no customer can install it. See [Salesforce orgs needed](/provider-guides/salesforce#salesforce-orgs-needed). + +## `Object type is not supported` for a standard object + +A read or write fails with an error similar to: + +```text +Object type 'contact' is not supported. If you are attempting to use a custom object, +be sure to append the '__c' after the entity name. +``` + +When the object named is a standard Salesforce object, this error is misleading. It usually means the connected user does not have permission to access the object, so Salesforce reports it as though it does not exist. + +Ask your customer's Salesforce admin to check the connected user's object permissions: + +- For a standard profile, see [Field permissions for a standard profile](/customer-guides/salesforce#2a-field-permissions-for-a-standard-profile). +- For a custom profile, see [Object and field permissions for a custom profile](/customer-guides/salesforce#2b-object-and-field-permissions-for-a-custom-profile). + +## Check Salesforce API quota + +When you suspect a customer is hitting Salesforce API limits, you can read their remaining quota through the proxy: + +```bash +curl --request GET "https://proxy.withampersand.com/services/data/v62.0/limits" \ + --header "Content-Type: application/json" \ + --header "x-amp-proxy-version: 1" \ + --header "x-amp-project-id: YOUR_PROJECT_ID" \ + --header "x-api-key: YOUR_API_KEY" \ + --header "x-amp-integration-name: YOUR_INTEGRATION" \ + --header "x-amp-group-ref: GROUP_REF" +``` + +The response includes `DailyApiRequests`, which shows the org's daily allocation and how much of it remains. diff --git a/src/troubleshooting.css b/src/troubleshooting.css new file mode 100644 index 00000000..ca52bd2f --- /dev/null +++ b/src/troubleshooting.css @@ -0,0 +1,83 @@ +/* Troubleshooting index (troubleshooting-guides/overview.mdx). Scoped under .amp-ts. */ + +.amp-ts { + --ts-fg: #19171c; + --ts-muted: #525055; + --ts-muted-2: #727075; + --ts-faint: #a09fa4; + --ts-accent: #6122E7; + --ts-surface: #ffffff; + --ts-surface-2: #f5f3f8; + --ts-border: rgba(9, 9, 11, 0.1); + --ts-border-2: #f0eef3; + --ts-border-input: #e0dfe4; + --ts-border-hover: #d0ced3; + margin-top: 1.5rem; + letter-spacing: -0.0125rem; + --ts-mono: var(--font-family-mono-custom, var(--font-jetbrains-mono, ui-monospace)), "SF Mono", SFMono-Regular, Menlo, monospace; +} +html.dark .amp-ts { + --ts-fg: #e0dfe4; + --ts-muted: #a09fa4; + --ts-muted-2: #8b898f; + --ts-faint: #525055; + --ts-accent: #A67CFF; + --ts-surface: #0f1117; + --ts-surface-2: #16161c; + --ts-border: rgba(255, 255, 255, 0.1); + --ts-border-2: #1f1e24; + --ts-border-input: #2e2d33; + --ts-border-hover: #403f44; +} + +.amp-ts [hidden] { display: none !important; } +.amp-ts-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } +.amp-ts input.amp-ts-search { + flex: 1; + min-width: 240px; + height: 38px; + padding: 0 12px; + border-radius: 12px; + border: 1px solid var(--ts-border-input); + background: var(--ts-surface-2); + color: var(--ts-fg); + font-size: 15px; + outline: none; + transition: border-color 120ms ease; +} +.amp-ts input.amp-ts-search:focus { border-color: var(--ts-accent); } +.amp-ts input.amp-ts-search::placeholder { color: var(--ts-faint); } +.amp-ts-chips { display: flex; flex-wrap: wrap; gap: 8px; } +.amp-ts button.amp-ts-chip { + height: 32px; + padding: 0 12px; + border: 1px solid var(--ts-border); + border-radius: 12px; + background: var(--ts-surface); + color: var(--ts-muted); + font-size: 14px; + font-family: inherit; + cursor: pointer; + transition: border-color 120ms ease, color 120ms ease; +} +.amp-ts button.amp-ts-chip:hover { border-color: var(--ts-border-hover); color: var(--ts-fg); } +.amp-ts button.amp-ts-chip.is-active { border-color: var(--ts-accent); color: var(--ts-accent); } + +.amp-ts-list { margin-top: 6px; } +.amp-ts a.amp-ts-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 16px; + padding: 14px 4px; + border-bottom: 1px solid var(--ts-border-2); + color: inherit; + text-decoration: none; + border-radius: 0; +} +.amp-ts a.amp-ts-row:hover .amp-ts-title { color: var(--ts-accent); } +.amp-ts-title { font-size: 16px; font-weight: 500; color: var(--ts-fg); transition: color 120ms ease; } +.amp-ts-title code { font-size: 13px; } +.amp-ts-desc { margin-top: 4px; font-size: 15px; line-height: 1.6; color: var(--ts-muted); } +.amp-ts-tag { font-family: var(--ts-mono); font-size: 12px; color: var(--ts-muted-2); white-space: nowrap; } +.amp-ts-count { margin-top: 12px; font-family: var(--ts-mono); font-size: 12px; color: var(--ts-muted-2); } diff --git a/src/troubleshooting.js b/src/troubleshooting.js new file mode 100644 index 00000000..87801d7f --- /dev/null +++ b/src/troubleshooting.js @@ -0,0 +1,64 @@ +// Search + provider filter on the troubleshooting index +// (troubleshooting-guides/overview.mdx). Guarded so it only acts on that page; +// Mintlify auto-includes every .js file in the content directory. +(function () { + function render(root) { + var input = root.querySelector("#amp-ts-search"); + var list = root.querySelector("#amp-ts-list"); + var count = root.querySelector("#amp-ts-count"); + if (!input || !list || !count) return; + + var active = root.querySelector(".amp-ts-chip.is-active"); + var tag = active ? active.getAttribute("data-tag") || "" : ""; + var q = (input.value || "").trim().toLowerCase(); + var rows = list.querySelectorAll(".amp-ts-row"); + var shown = 0; + + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + var tags = (row.getAttribute("data-tags") || "").toLowerCase(); + var text = (row.textContent || "").toLowerCase(); + var hit = (!tag || tags.indexOf(tag) !== -1) && (!q || text.indexOf(q) !== -1 || tags.indexOf(q) !== -1); + row.hidden = !hit; + if (hit) shown++; + } + + count.textContent = shown === rows.length + ? "Showing all " + rows.length + " entries" + : shown + " of " + rows.length + " entries match"; + } + + function init() { + var root = document.getElementById("amp-ts-root"); + if (!root || root.getAttribute("data-ts-ready") === "1") return; + root.setAttribute("data-ts-ready", "1"); + render(root); + } + + document.addEventListener("input", function (e) { + if (e.target && e.target.id === "amp-ts-search") { + var root = e.target.closest(".amp-ts"); + if (root) render(root); + } + }); + + document.addEventListener("click", function (e) { + var chip = e.target && e.target.closest ? e.target.closest(".amp-ts-chip") : null; + if (!chip) return; + var root = chip.closest(".amp-ts"); + if (!root) return; + var chips = root.querySelectorAll(".amp-ts-chip"); + for (var i = 0; i < chips.length; i++) chips[i].classList.remove("is-active"); + chip.classList.add("is-active"); + render(root); + }); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + if (typeof MutationObserver !== "undefined") { + new MutationObserver(init).observe(document.documentElement, { childList: true, subtree: true }); + } +})();