diff --git a/bun.lock b/bun.lock index 8f4a63510..fdc19baf2 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ }, "integrations/ahrefs": { "name": "@gitbook/integration-ahrefs", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -381,9 +381,21 @@ "esbuild": "^0.15.7", }, }, + "integrations/matomo": { + "name": "@gitbook/integration-matomo", + "version": "0.1.0", + "dependencies": { + "@gitbook/api": "*", + "@gitbook/runtime": "*", + }, + "devDependencies": { + "@gitbook/cli": "workspace:*", + "@gitbook/tsconfig": "workspace:*", + }, + }, "integrations/mermaid": { "name": "@gitbook/integration-mermaid", - "version": "0.4.0", + "version": "0.4.2", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -433,7 +445,7 @@ }, "integrations/plausible": { "name": "@gitbook/integration-plausible", - "version": "0.7.0", + "version": "0.8.0", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -507,7 +519,7 @@ }, "integrations/segment": { "name": "@gitbook/integration-segment", - "version": "2.1.3", + "version": "2.3.0", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -652,7 +664,7 @@ }, "packages/api": { "name": "@gitbook/api", - "version": "0.135.0", + "version": "0.136.0", "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0", @@ -1035,6 +1047,8 @@ "@gitbook/integration-marketo": ["@gitbook/integration-marketo@workspace:integrations/marketo"], + "@gitbook/integration-matomo": ["@gitbook/integration-matomo@workspace:integrations/matomo"], + "@gitbook/integration-mermaid": ["@gitbook/integration-mermaid@workspace:integrations/mermaid"], "@gitbook/integration-mixpanel": ["@gitbook/integration-mixpanel@workspace:integrations/mixpanel"], diff --git a/integrations/matomo/CHANGELOG.md b/integrations/matomo/CHANGELOG.md new file mode 100644 index 000000000..fdda3b53c --- /dev/null +++ b/integrations/matomo/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## 0.1.0 + +- Initial release of the Matomo integration for GitBook. Injects a lightweight tracker using the Matomo Tracking API with SPA navigation support and cookie consent awareness. + +## 0.1.1 + +- Update icon to square, icon-only mark for better marketplace display +- Add preview image +- Switch visibility to `public` for marketplace readiness +- Improve manifest descriptions and validation to meet CLI limits diff --git a/integrations/matomo/assets/icon.png b/integrations/matomo/assets/icon.png new file mode 100644 index 000000000..114d33800 Binary files /dev/null and b/integrations/matomo/assets/icon.png differ diff --git a/integrations/matomo/assets/preview.png b/integrations/matomo/assets/preview.png new file mode 100644 index 000000000..c3d33ed0a Binary files /dev/null and b/integrations/matomo/assets/preview.png differ diff --git a/integrations/matomo/gitbook-manifest.yaml b/integrations/matomo/gitbook-manifest.yaml new file mode 100644 index 000000000..6e0646693 --- /dev/null +++ b/integrations/matomo/gitbook-manifest.yaml @@ -0,0 +1,91 @@ +name: matomo +title: Matomo +icon: ./assets/icon.png +previewImages: + - ./assets/preview.png +organization: 2zoWGxtV7bjhbwBdjGPS +description: Receive GitBook traffic insights directly in your Matomo dashboard. +visibility: public +script: ./src/index.ts +# The following scope(s) are available only to GitBook Staff +# See https://developer.gitbook.com/integrations/configurations#scopes +scopes: + - site:script:inject + - site:script:cookies +categories: + - analytics +contentSecurityPolicy: + # Allow loading Matomo JS tracker and sending beacons to user-provided hosts + script-src: | + *; + img-src: | + *; + connect-src: | + *; +summary: | + # Overview + + Matomo (formerly Piwik) is a powerful open‑source web analytics platform. This integration lets you track visits to your published GitBook spaces directly in your Matomo instance (cloud or self‑hosted). + + # How it works + + The integration injects a lightweight tracking script that respects GitBook's cookie consent. When consent is granted, it sends pageview events to your configured Matomo server using the Matomo Tracking API. Single‑page navigation within GitBook is tracked automatically. + + # Configure + + Install the integration on the GitBook space(s) you want to track, then provide your Matomo server URL and Site ID. +externalLinks: + - label: Matomo developer docs + url: https://developer.matomo.org/integration +configurations: + site: + properties: + server_url: + type: string + title: Matomo Server URL + description: | + Your Matomo server base URL. Examples: https://your-site.matomo.cloud or https://matomo.example.com + site_id: + type: string + title: Site ID + description: The numeric site ID configured in your Matomo instance + load_js_tracker: + type: boolean + title: Load Matomo JS tracker + description: | + When enabled, loads matomo.js for first‑party cookies and richer tracking. + default: true + track_referrer: + type: boolean + title: Track Referrer + description: | + Include document.referrer for pageviews/events (recommended) + default: true + track_outbound_clicks: + type: boolean + title: Track outbound link clicks + description: | + Record clicks on links pointing to external domains + default: true + click_selectors: + type: string + title: CSS selectors to track + description: | + Comma‑separated CSS selectors to track clicks as events. Example: a[href*="try"],button.book-demo + default: '' + goal_mappings_json: + type: string + title: Goal mappings (JSON) + description: | + Optional JSON object mapping CSS selectors to Matomo Goal IDs. + default: '' + user_id_cookie: + type: string + title: User ID cookie name + description: | + Optional cookie name to read and set as Matomo user ID (for retention/cohort analysis) + default: '' + required: + - server_url + - site_id +target: site diff --git a/integrations/matomo/global.d.ts b/integrations/matomo/global.d.ts new file mode 100644 index 000000000..07471766a --- /dev/null +++ b/integrations/matomo/global.d.ts @@ -0,0 +1,12 @@ +declare module '*.raw.js' { + const content: string; + export default content; +} + +// Allow local typechecking without installed workspace deps +declare module '@gitbook/runtime' { + export type FetchPublishScriptEventCallback = any; + export type RuntimeContext = any; + export type RuntimeEnvironment = any; + export function createIntegration(...args: any[]): any; +} diff --git a/integrations/matomo/package.json b/integrations/matomo/package.json new file mode 100644 index 000000000..f39f284a0 --- /dev/null +++ b/integrations/matomo/package.json @@ -0,0 +1,19 @@ +{ + "name": "@gitbook/integration-matomo", + "version": "0.1.0", + "private": true, + "dependencies": { + "@gitbook/api": "*", + "@gitbook/runtime": "*" + }, + "devDependencies": { + "@gitbook/cli": "workspace:*", + "@gitbook/tsconfig": "workspace:*" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "publish-integrations-staging": "gitbook publish .", + "check": "gitbook check", + "publish-integrations": "gitbook publish ." + } +} diff --git a/integrations/matomo/src/index.ts b/integrations/matomo/src/index.ts new file mode 100644 index 000000000..eee4382ac --- /dev/null +++ b/integrations/matomo/src/index.ts @@ -0,0 +1,58 @@ +import { + createIntegration, + FetchPublishScriptEventCallback, + RuntimeContext, + RuntimeEnvironment, +} from '@gitbook/runtime'; + +import matomoScript from './matomoScript.raw.js'; + +type MatomoRuntimeContext = RuntimeContext< + RuntimeEnvironment< + {}, + { + server_url?: string; + site_id?: string; + load_js_tracker?: boolean; + track_referrer?: boolean; + track_outbound_clicks?: boolean; + click_selectors?: string; + goal_mappings_json?: string; + user_id_cookie?: string; + } + > +>; + +export const handleFetchEvent: FetchPublishScriptEventCallback = async ( + event: any, + { environment }: MatomoRuntimeContext, +) => { + const cfg = environment.siteInstallation?.configuration || {}; + const serverUrl = cfg.server_url; + const siteId = cfg.site_id; + + if (!serverUrl || !siteId) { + return; + } + + const js = (matomoScript as string) + .replace('', serverUrl.replace(/\/$/, '')) + .replace('', String(siteId)) + .replace('', String(cfg.load_js_tracker ?? true)) + .replace('', String(cfg.track_referrer ?? true)) + .replace('', String(cfg.track_outbound_clicks ?? true)) + .replace('', String(cfg.click_selectors ?? '')) + .replace('', String(cfg.goal_mappings_json ?? '')) + .replace('', String(cfg.user_id_cookie ?? '')); + + return new Response(js, { + headers: { + 'Content-Type': 'application/javascript', + 'Cache-Control': 'max-age=604800', + }, + }); +}; + +export default createIntegration({ + fetch_published_script: handleFetchEvent, +}); diff --git a/integrations/matomo/src/matomoScript.raw.js b/integrations/matomo/src/matomoScript.raw.js new file mode 100644 index 000000000..3990c5d9d --- /dev/null +++ b/integrations/matomo/src/matomoScript.raw.js @@ -0,0 +1,223 @@ +(function () { + var MATOMO_URL = ''; + var SITE_ID = ''; + var LOAD_JS_TRACKER = '' === 'true'; + var TRACK_REFERRER = '' === 'true'; + var TRACK_OUTBOUND = '' === 'true'; + var CLICK_SELECTORS = ''; + var GOAL_MAPPINGS_JSON = ''; + var USER_ID_COOKIE = ''; + var GRANTED_COOKIE = '__gitbook_cookie_granted'; + + function getCookie(name) { + var nameEQ = name + '='; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return ''; + } + + function hasConsent() { + return getCookie(GRANTED_COOKIE) === 'yes'; + } + + function urlParams(baseUrl, params) { + return ( + baseUrl + + '?' + + Object.keys(params) + .filter(function (k) { + return params[k] !== undefined && params[k] !== '' && params[k] !== null; + }) + .map(function (k) { + return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); + }) + .join('&') + ); + } + + function sendBeacon(params) { + if (!hasConsent()) return; + try { + var url = MATOMO_URL + '/matomo.php'; + var beacon = new Image(); + beacon.src = urlParams(url, params); + } catch (e) {} + } + + function getReferrer() { + return TRACK_REFERRER ? document.referrer || '' : ''; + } + + function getUserId() { + if (!USER_ID_COOKIE) return ''; + return getCookie(USER_ID_COOKIE) || ''; + } + + function trackPageview() { + if (LOAD_JS_TRACKER && window._paq) { + window._paq.push(['setReferrerUrl', getReferrer()]); + var uid = getUserId(); + if (uid) window._paq.push(['setUserId', uid]); + window._paq.push(['setDocumentTitle', document.title]); + window._paq.push(['setCustomUrl', window.location.href]); + window._paq.push(['trackPageView']); + return; + } + // Beacon fallback + sendBeacon({ + idsite: SITE_ID, + rec: 1, + url: window.location.href, + action_name: document.title, + urlref: getReferrer(), + uid: getUserId(), + rand: String(Math.random()).slice(2), + }); + } + + function trackEvent(category, action, name, value) { + if (LOAD_JS_TRACKER && window._paq) { + var uid = getUserId(); + if (uid) window._paq.push(['setUserId', uid]); + if (TRACK_REFERRER) window._paq.push(['setReferrerUrl', getReferrer()]); + window._paq.push(['trackEvent', category, action, name, value]); + return; + } + sendBeacon({ + idsite: SITE_ID, + rec: 1, + e_c: category, + e_a: action, + e_n: name, + e_v: value, + url: window.location.href, + urlref: getReferrer(), + uid: getUserId(), + rand: String(Math.random()).slice(2), + }); + } + + function bindClicks() { + // Track configured selectors + if (CLICK_SELECTORS && CLICK_SELECTORS.trim()) { + try { + var selectors = CLICK_SELECTORS.split(','); + selectors.forEach(function (sel) { + sel = sel.trim(); + if (!sel) return; + document.addEventListener('click', function (e) { + var target = e.target; + if (!(target instanceof Element)) return; + var el = target.closest(sel); + if (!el) return; + var label = (el.textContent || el.getAttribute('aria-label') || '').trim(); + var href = el.getAttribute && el.getAttribute('href'); + trackEvent('Click', 'click', label || href || sel); + }); + }); + } catch (e) {} + } + + // Track outbound links if enabled + if (TRACK_OUTBOUND) { + document.addEventListener('click', function (e) { + var target = e.target; + if (!(target instanceof Element)) return; + var link = target.closest('a[href]'); + if (!link) return; + try { + var url = new URL(link.href, window.location.href); + if (url.host && url.host !== window.location.host) { + trackEvent('Outbound', 'click', url.href); + } + } catch (err) {} + }); + } + } + + function applyGoals() { + if (!GOAL_MAPPINGS_JSON) return; + var mappings; + try { + mappings = JSON.parse(GOAL_MAPPINGS_JSON); + } catch (e) { + return; + } + if (!mappings || typeof mappings !== 'object') return; + document.addEventListener('click', function (e) { + var target = e.target; + if (!(target instanceof Element)) return; + for (var sel in mappings) { + var goalId = mappings[sel]; + if (!goalId) continue; + var el = target.closest(sel); + if (el) { + if (LOAD_JS_TRACKER && window._paq) { + window._paq.push(['trackGoal', Number(goalId)]); + } else { + sendBeacon({ + idsite: SITE_ID, + rec: 1, + idgoal: Number(goalId), + rand: String(Math.random()).slice(2), + }); + } + break; + } + } + }); + } + + function loadJS() { + if (!LOAD_JS_TRACKER) return; + if (!hasConsent()) return; + var _paq = (window._paq = window._paq || []); + var uid = getUserId(); + if (uid) _paq.push(['setUserId', uid]); + if (TRACK_REFERRER) _paq.push(['setReferrerUrl', getReferrer()]); + _paq.push(['enableLinkTracking']); + _paq.push(['setTrackerUrl', MATOMO_URL + '/matomo.php']); + _paq.push(['setSiteId', SITE_ID]); + + var g = document.createElement('script'); + g.async = true; + g.src = MATOMO_URL + '/matomo.js'; + g.onload = function () { + // Track initial after JS ready + trackPageview(); + }; + var s = document.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(g, s); + } + + // Detect SPA navigation and track + var lastPathname; + function trackIfPathChanged() { + if (lastPathname !== window.location.pathname) { + lastPathname = window.location.pathname; + trackPageview(); + } + } + + // Init + bindClicks(); + applyGoals(); + loadJS(); + trackPageview(); + + var originalPushState = window.history.pushState; + if (originalPushState) { + window.history.pushState = new Proxy(originalPushState, { + apply: function (target, thisArg, args) { + var result = target.apply(thisArg, args); + trackIfPathChanged(); + return result; + }, + }); + } + window.addEventListener('popstate', trackIfPathChanged); +})(); diff --git a/integrations/matomo/tsconfig.json b/integrations/matomo/tsconfig.json new file mode 100644 index 000000000..483a24a75 --- /dev/null +++ b/integrations/matomo/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "jsxImportSource": "@gitbook/runtime", + "strict": true, + "skipLibCheck": true, + "types": [] + } +}