Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/actions/setup-fern/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ runs:
- name: Install pinned Fern
shell: bash
run: |
npm install --prefix "$RUNNER_TEMP/vapi-fern" --no-save --no-audit --no-fund fern-api@5.151.1
npm install --prefix "$RUNNER_TEMP/vapi-fern" --no-save --no-audit --no-fund fern-api@5.169.0
echo "FERN_CLI_PATH=$RUNNER_TEMP/vapi-fern/node_modules/fern-api/cli.cjs" >> "$GITHUB_ENV"
- name: Test cache safeguards
shell: bash
run: node --test scripts/fern/example-cache.test.cjs
run: node --test scripts/fern/*.test.cjs
- uses: actions/cache@v4
with:
path: .cache/fern-examples
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,25 @@ jobs:

- name: Check API is valid
run: node scripts/fern/run.cjs check

docs-consent:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-fern
- name: Install browser regression dependencies
run: npm ci --ignore-scripts --no-audit --no-fund
- name: Install Chromium
run: npx playwright install --with-deps chromium
- name: Test consent and subscription logic
run: |
npm run test:privacy
node fern/custom.spec.js
- name: Verify generated local Fern consent behavior
run: npm run test:privacy:browser
- uses: actions/upload-artifact@v4
if: always()
with:
name: docs-consent-requests
path: ${{ runner.temp }}/docs-privacy-results
retention-days: 7
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,22 @@ You can suggest edits by making a pull request.
To run a local development server with hot-reloading you can run the following command

```sh
npm install -g fern-api
npm install -g fern-api@5.169.0
fern docs dev
```

Consent behavior, regression checks, publication prerequisites, and safe disablement are documented in the [docs consent runbook](scripts/privacy/README.md).

#### Hosted URL

To update your documentation on a hosted URL, run
```
# npm install -g fern-api
# npm install -g fern-api@5.169.0
fern generate --docs
```
To preview your documentation, run
```
# npm install -g fern-api
# npm install -g fern-api@5.169.0
fern generate --docs --preview
```
The repository contains GitHub workflows that will automatically run these commands for you. For example, when you make a PR a preview link will be auto-generated and when you merge to main the docs site will update.
6 changes: 3 additions & 3 deletions advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This repository contains our Fern Configuration:
## Setup

```sh
npm install -g fern-api
npm install -g fern-api@5.169.0
```

## Validating your OpenAPI Specs
Expand Down Expand Up @@ -59,12 +59,12 @@ fern docs dev

To update your documentation on a hosted URL, run
```
# npm install -g fern-api
# npm install -g fern-api@5.169.0
fern generate --docs
```
To preview your documentation, run
```
# npm install -g fern-api
# npm install -g fern-api@5.169.0
fern generate --docs --preview
```
The repository contains GitHub workflows that will automatically run these commands for you. For example, when you make a PR a preview link will be auto-generated and when you merge to main the docs site will update.
58 changes: 58 additions & 0 deletions fern/assets/privacy-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Necessary Vapi API requests still run when optional collection is denied.
// Stamp only the audited API origins, at dispatch, without modifying auth,
// credentials, body, signal, or other caller headers.
(function () {
if (window.__vapiDocsConsentRequests) return;
window.__vapiDocsConsentRequests = true;
function applicable(input) {
try {
var url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url, window.location.href);
return url.origin === 'https://api.vapi.ai' || url.origin === 'https://api.eu.vapi.ai';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two origins now get X-Vapi-Consent-Analytics / X-Vapi-Consent-Marketing on every fetch and XHR, so the browser sends an OPTIONS preflight first. Please confirm on the docs preview that a playground call to each origin passes preflight in production (a screenshot of the network tab or the Playwright request log is enough). If either rejects the headers, the fix is on the API side before this merges, not here.

} catch (_) { return false; }
}
function values() {
var state = window.vapiDocsConsent && window.vapiDocsConsent.getSnapshot();
return {
'X-Vapi-Consent-Analytics': state && state.ready && state.analytics ? 'granted' : 'denied',
'X-Vapi-Consent-Marketing': state && state.ready && state.marketing ? 'granted' : 'denied'
};
}
if (typeof window.fetch === 'function') {
var fetch = window.fetch;
window.fetch = function (input, init) {
if (!applicable(input)) return fetch.call(this, input, init);
var source = init && init.headers !== undefined ? init.headers : input && input.headers;
var headers = new Headers(source);
var consent = values();
Object.keys(consent).forEach(function (name) { headers.set(name, consent[name]); });
return fetch.call(this, input, Object.assign({}, init, { headers: headers }));
};
}
if (typeof XMLHttpRequest !== 'undefined') {
var targets = new WeakMap();
var open = XMLHttpRequest.prototype.open;
var setHeader = XMLHttpRequest.prototype.setRequestHeader;
var send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function () {
// open() can synchronously fire readystatechange; callers may send from
// that callback, so classify before entering the native method.
targets.set(this, applicable(arguments[1]));
try { return open.apply(this, arguments); }
catch (error) { targets.delete(this); throw error; }
};
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
// These two reserved headers are adapter-owned. Avoid XHR appending a
// stale caller grant to the current value supplied immediately at send.
if (targets.get(this) && /^x-vapi-consent-(analytics|marketing)$/i.test(name)) return;
return setHeader.call(this, name, value);
};
XMLHttpRequest.prototype.send = function () {
if (targets.get(this)) {
var consent = values();
var xhr = this;
Object.keys(consent).forEach(function (name) { setHeader.call(xhr, name, consent[name]); });
}
return send.apply(this, arguments);
};
}
})();
86 changes: 86 additions & 0 deletions fern/assets/privacy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Necessary consent control only. Optional SDKs must use this snapshot; do not
// add loaders here without verified startup-race and withdrawal controls.
(function () {
if (window.vapiDocsConsent) return;

var snapshot = Object.freeze({ ready: false, analytics: false, marketing: false, personalization: false, generation: 0 });
var listeners = new Set();
var pendingDenials = {};
var fernGranted = false;
var fernWithdrawn = false;
window.fernAnalyticsConsent = false;

// This adapter is the only application Google-consent writer. The Osano
// automatic bridge must be disabled in the release configuration (runbook).
window.dataLayer = window.dataLayer || [];
function googleConsent(command, state) {
function gtag() { window.dataLayer.push(arguments); }
gtag('consent', command, {
analytics_storage: state.analytics ? 'granted' : 'denied',
ad_storage: state.marketing ? 'granted' : 'denied',
ad_user_data: state.marketing ? 'granted' : 'denied',
ad_personalization: state.marketing ? 'granted' : 'denied',
personalization_storage: state.personalization ? 'granted' : 'denied'
});
}
googleConsent('default', snapshot);

window.vapiDocsConsent = Object.freeze({
getSnapshot: function () { return snapshot; },
subscribe: function (listener) {
listeners.add(listener);
return function () { listeners.delete(listener); };
}
});

function readConsent() {
try { return window.Osano.cm.getConsent(); } catch (_) { return undefined; }
}
function valid(value) { return value === 'ACCEPT' || value === 'DENY'; }
function reconcile(consent) {
consent = consent || readConsent();
if (consent) consent = Object.assign({}, consent, pendingDenials);
var ready = !!consent && valid(consent.ANALYTICS) && valid(consent.MARKETING) && valid(consent.PERSONALIZATION);
var next = {
ready: ready,
analytics: ready && consent.ANALYTICS === 'ACCEPT',
marketing: ready && consent.MARKETING === 'ACCEPT' && consent['OPT-OUT'] !== 'ACCEPT' && consent.OPT_OUT !== 'ACCEPT' && navigator.globalPrivacyControl !== true,
personalization: ready && consent.PERSONALIZATION === 'ACCEPT',
generation: snapshot.generation + 1
};
if (next.ready === snapshot.ready && next.analytics === snapshot.analytics && next.marketing === snapshot.marketing && next.personalization === snapshot.personalization) return;
snapshot = Object.freeze(next);
// Fern's native gate reads the property, not event.detail. Latch withdrawal
// even during SDK import/idle startup: no regrant on this page after a grant.
if (fernGranted && !next.analytics) fernWithdrawn = true;
var allowFern = next.analytics && !fernWithdrawn;
if (allowFern) fernGranted = true;
window.fernAnalyticsConsent = allowFern;
window.dispatchEvent(new Event('fern-analytics-consent'));
googleConsent('update', next);
listeners.forEach(function (listener) { listener(snapshot); });
}

// Official preload queue closes the late-CMP race without a polling timeout.
window.Osano = window.Osano || function () { window.Osano.data.push(arguments); };
window.Osano.data = window.Osano.data || [];
window.Osano('onInitialized', reconcile);
window.Osano('onConsentSaved', function (consent) {
pendingDenials = {};
reconcile(consent);
});
window.Osano('onOptOut', function () { reconcile(); });
window.Osano('onConsentChanged', function (change) {
// Unsaved acceptance is not a grant. A toggle to denial takes effect now,
// even if getConsent() still reports the last saved state.
var consent = readConsent();
if (change && consent) {
['ANALYTICS', 'MARKETING', 'PERSONALIZATION'].forEach(function (category) {
if (change[category] === 'DENY') pendingDenials[category] = 'DENY';
});
if (change['OPT-OUT'] === 'ACCEPT') pendingDenials['OPT-OUT'] = 'ACCEPT';
if (change.OPT_OUT === 'ACCEPT') pendingDenials.OPT_OUT = 'ACCEPT';
}
reconcile(consent);
});
})();
109 changes: 0 additions & 109 deletions fern/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ const WIDGET_SCRIPT_URL = isLocalhost
? 'http://localhost:9001/widget.js'
: 'https://docs-widget.vercel.app/widget.js';

const HOCKEYSTACK_API_KEY = '96e358f635f3f5ea7fda26023b10da';
const REO_CLIENT_ID = '0dc28e3fda800b9';
const LINKEDIN_PARTNER_ID = '6951196';

function injectVapiWidget() {
console.log('[custom.js] injectVapiWidget called');
if (document.querySelector(WIDGET_TAG)) {
Expand Down Expand Up @@ -36,106 +32,6 @@ function injectVapiWidget() {
console.log('[custom.js] Widget script appended to DOM');
}

function initializeHockeyStack() {
if (isLocalhost) {
console.log('[custom.js] Skipping HockeyStack on localhost');
return;
}

var hsscript = document.createElement("script");
hsscript.id = "wphs";
hsscript.src = "https://cdn.jsdelivr.net/npm/hockeystack@latest/hockeystack.min.js";
hsscript.async = 1;
hsscript.dataset.apikey = HOCKEYSTACK_API_KEY;
hsscript.dataset.cookieless = 1;
hsscript.dataset.autoIdentify = 1;

document.getElementsByTagName('head')[0].append(hsscript);
}

function initializeReo() {
if (isLocalhost) {
console.log('[custom.js] Skipping Reo on localhost');
return;
}

var reoScript = document.createElement("script");
reoScript.type = "text/javascript";
reoScript.src = "https://static.reo.dev/" + REO_CLIENT_ID + "/reo.js";
reoScript.defer = true;
reoScript.onload = function() {
if (typeof Reo !== 'undefined') {
Reo.init({ clientID: REO_CLIENT_ID });
}
};
document.head.appendChild(reoScript);
}

function configurePostHog() {
if (isLocalhost) {
console.log('[custom.js] Skipping PostHog configuration on localhost');
return;
}

// Wait for PostHog to be initialized by Fern
const checkPostHog = setInterval(() => {
if (typeof window.posthog !== 'undefined') {
clearInterval(checkPostHog);

// Configure cross-domain tracking
window.posthog.set_config({
cross_subdomain_cookie: true,
cross_domain: '.vapi.ai',
persistence: 'localStorage+cookie'
});

}
}, 100);

}

function initializeHubSpot() {

if (isLocalhost) {
console.log('[custom.js] Skipping HubSpot configuration on localhost');
return;
}

var hubSpotScript = document.createElement("script");
hubSpotScript.type = "text/javascript";
hubSpotScript.id = "hs-script-loader";
hubSpotScript.src = "https://js-na2.hs-scripts.com/244349038.js";
hubSpotScript.async = true;
hubSpotScript.defer = true;
document.getElementsByTagName('head')[0].appendChild(hubSpotScript);
}

function initializeLinkedIn() {
if (isLocalhost) {
console.log('[custom.js] Skipping LinkedIn Insight Tag on localhost');
return;
}

window._linkedin_partner_id = LINKEDIN_PARTNER_ID;
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
window._linkedin_data_partner_ids.push(LINKEDIN_PARTNER_ID);

(function (l) {
if (!l) {
window.lintrk = function (a, b) {
window.lintrk.q.push([a, b]);
};
window.lintrk.q = [];
}
var s = document.getElementsByTagName('script')[0];
var b = document.createElement('script');
b.type = 'text/javascript';
b.async = true;
b.src = 'https://snap.licdn.com/li.lms-analytics/insight.min.js';
s.parentNode.insertBefore(b, s);
})(window.lintrk);
}

function initializeSubscribeForm() {
// Fern's MDX renderer strips JSX event handlers (onSubmit, onClick), so the
// form's validation and submission logic must be attached from plain JS.
Expand Down Expand Up @@ -222,11 +118,6 @@ function initializeSubscribeForm() {
}

function initializeAll() {
initializeHockeyStack();
initializeReo();
initializeHubSpot();
initializeLinkedIn();
configurePostHog();
initializeSubscribeForm();
if (ENABLE_VOICE_WIDGET) {
injectVapiWidget();
Expand Down
9 changes: 7 additions & 2 deletions fern/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ experimental:
mdx-components:
- snippets
settings:
require-analytics-consent: true
http-snippets: false
dark-mode-code: true
css: assets/styles.css
js:
- path: ./assets/privacy.js
strategy: beforeInteractive
- url: https://cmp.osano.com/18N6QTUelY/9fd9583a-77ec-4ecf-a147-069f96fcb514/osano.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same Osano config as vapi.ai and the dashboard, so docs pick up the owner's strict-mode, region and discovery changes too. Please re-run the browser suite on the preview after those are published, since the results here were against the current config.

strategy: beforeInteractive
- path: ./assets/privacy-requests.js
strategy: beforeInteractive
- path: ./assets/close-playground.js
- path: ./custom.js
navbar-links:
Expand Down Expand Up @@ -96,8 +103,6 @@ analytics:
posthog:
api-key: ${POSTHOG_PROJECT_API_KEY}
endpoint: https://us.i.posthog.com
ga4:
measurement-id: G-BVPB7XB842
navigation:
- tab: documentation
layout:
Expand Down
2 changes: 1 addition & 1 deletion fern/fern.config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"organization": "vapi",
"version": "5.151.1"
"version": "5.169.0"
}
Loading
Loading