Skip to content

Commit f076fce

Browse files
Add Osano integration (#1248)
* Add Osano integration * Fix Osano infinite reload loop by deduping consent decisions onConsentSaved replays the visitor's existing decision on every page load, and GitBook's onApprove/onReject can trigger a reload, so every replay caused another reload in an infinite loop. Only forward the decision when it actually changes, tracked via sessionStorage so it survives the reload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 76fa2cb commit f076fce

10 files changed

Lines changed: 221 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/integration-osano': minor
3+
---
4+
5+
Add Osano cookie consent integration.

bun.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integrations/osano/assets/icon.png

26.4 KB
Loading
359 KB
Loading
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: osano
2+
title: Osano Cookie Consent
3+
description: Add the Osano cookie consent banner to your published GitBook site.
4+
visibility: public
5+
organization: gitbook
6+
icon: ./assets/icon.png
7+
previewImages:
8+
- ./assets/osano-preview.png
9+
script: ./src/index.ts
10+
# The following scope(s) are available only to GitBook Staff
11+
# See https://developer.gitbook.com/integrations/configurations#scopes
12+
scopes:
13+
- site:script:inject
14+
- site:script:cookies
15+
contentSecurityPolicy:
16+
script-src: |
17+
https://cmp.osano.com;
18+
summary: |
19+
# Overview
20+
This integration allows you to add the Osano cookie consent banner on your published GitBook site.
21+
22+
# How it works
23+
The integration injects the Osano CMP script `osano.js` on your page, using your Customer ID and
24+
Configuration ID, so that the cookie consent banner is displayed and consent is managed according
25+
to your Osano configuration.
26+
27+
# Configure
28+
Install the integration on the GitBook site of your choice.
29+
Provide your Osano Customer ID and Configuration ID. These required values are provided by Osano
30+
and correspond to the two IDs in your `osano.js` script URL: `https://cmp.osano.com/<Customer ID>/<Configuration ID>/osano.js`.
31+
categories:
32+
- analytics
33+
configurations:
34+
site:
35+
properties:
36+
customer_id:
37+
type: string
38+
title: Customer ID
39+
description: The Osano Customer ID, the first ID segment in your osano.js script URL
40+
config_id:
41+
type: string
42+
title: Configuration ID
43+
description: The Osano Configuration ID, the second ID segment in your osano.js script URL
44+
required:
45+
- customer_id
46+
- config_id
47+
target: site

integrations/osano/global.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare module '*.raw.js' {
2+
const content: string;
3+
export default content;
4+
}
5+
6+
export declare global {
7+
interface Window {
8+
GitBook: import('@gitbook/browser-types').GitBookGlobal;
9+
}
10+
}

integrations/osano/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@gitbook/integration-osano",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"@gitbook/api": "*",
7+
"@gitbook/runtime": "*"
8+
},
9+
"devDependencies": {
10+
"@gitbook/cli": "workspace:*",
11+
"@gitbook/tsconfig": "workspace:*",
12+
"@gitbook/browser-types": "^0.1.5"
13+
},
14+
"scripts": {
15+
"typecheck": "tsc --noEmit",
16+
"publish-integrations-staging": "gitbook publish .",
17+
"check": "gitbook check",
18+
"publish-integrations": "gitbook publish ."
19+
}
20+
}

integrations/osano/src/index.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
createIntegration,
3+
FetchPublishScriptEventCallback,
4+
RuntimeContext,
5+
RuntimeEnvironment,
6+
} from '@gitbook/runtime';
7+
8+
import script from './script.raw.js';
9+
10+
type OsanoRuntimeContext = RuntimeContext<
11+
RuntimeEnvironment<
12+
{},
13+
{
14+
customer_id?: string;
15+
config_id?: string;
16+
}
17+
>
18+
>;
19+
20+
export const handleFetchEvent: FetchPublishScriptEventCallback = async (
21+
event,
22+
{ environment }: OsanoRuntimeContext,
23+
) => {
24+
const customerId = environment.siteInstallation?.configuration?.customer_id;
25+
const configId = environment.siteInstallation?.configuration?.config_id;
26+
27+
if (!customerId || !configId) {
28+
return;
29+
}
30+
31+
return new Response(
32+
(script as string).replace('<CUSTOMER_ID>', customerId).replace('<CONFIG_ID>', configId),
33+
{
34+
headers: {
35+
'Content-Type': 'application/javascript',
36+
'Cache-Control': 'max-age=604800',
37+
},
38+
},
39+
);
40+
};
41+
42+
export default createIntegration<OsanoRuntimeContext>({
43+
fetch_published_script: handleFetchEvent,
44+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
(function () {
2+
var w = window;
3+
var d = document;
4+
5+
var NON_ESSENTIAL_CATEGORIES = ['ANALYTICS', 'MARKETING', 'PERSONALIZATION', 'STORAGE'];
6+
7+
// Registers the window.Osano('eventName', callback) pre-load queue so listeners
8+
// added before osano.js has loaded are replayed once the real API is ready.
9+
// See https://developers.osano.com/cmp/javascript-api/developer-documentation-consent-javascript-api#pre-load
10+
function setupOsanoPreload() {
11+
if (typeof w.Osano === 'function') return;
12+
w.Osano = function () {
13+
w.Osano.data.push(arguments);
14+
};
15+
w.Osano.data = [];
16+
}
17+
18+
function injectOsano() {
19+
var stub = d.getElementById('osano-sdk-stub');
20+
if (stub) return;
21+
22+
var s = d.createElement('script');
23+
s.id = 'osano-sdk-stub';
24+
s.type = 'text/javascript';
25+
s.async = true;
26+
s.src = 'https://cmp.osano.com/<CUSTOMER_ID>/<CONFIG_ID>/osano.js';
27+
d.head.appendChild(s);
28+
}
29+
30+
function l() {
31+
if (!w.GitBook || typeof w.GitBook.registerCookieBanner !== 'function') return;
32+
33+
w.GitBook.registerCookieBanner(function ({ onApprove, onReject }) {
34+
setupOsanoPreload();
35+
36+
var CONSENT_STORAGE_KEY = 'osano-gitbook-last-consent-decision';
37+
38+
function emitConsent(consent) {
39+
try {
40+
var hasNonEssential =
41+
!!consent &&
42+
NON_ESSENTIAL_CATEGORIES.some(function (category) {
43+
return consent[category] === 'ACCEPT';
44+
});
45+
var decision = hasNonEssential ? 'approve' : 'reject';
46+
47+
// onConsentSaved replays the visitor's existing decision on every
48+
// page load, not just when it changes. GitBook's onApprove/onReject
49+
// can trigger a reload to reinitialize scripts, so forwarding every
50+
// replay would reload -> replay -> reload forever. Only forward the
51+
// decision when it's actually different from last time, and persist
52+
// that in sessionStorage since it must survive the reload.
53+
if (w.sessionStorage.getItem(CONSENT_STORAGE_KEY) === decision) return;
54+
w.sessionStorage.setItem(CONSENT_STORAGE_KEY, decision);
55+
56+
if (hasNonEssential) {
57+
onApprove();
58+
} else {
59+
onReject();
60+
}
61+
} catch (e) {
62+
onReject();
63+
}
64+
}
65+
66+
w.Osano('onConsentSaved', emitConsent);
67+
68+
injectOsano();
69+
});
70+
}
71+
72+
if (w.attachEvent) {
73+
w.attachEvent('onload', l);
74+
} else {
75+
w.addEventListener('load', l, false);
76+
}
77+
})();

integrations/osano/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@gitbook/tsconfig/integration.json"
3+
}

0 commit comments

Comments
 (0)