addEventListener('fetch', event => { event.respondWith(handle(event)) }) /** * Steps to use: * - Create CF Worker, copy and paste this in * - If you want dynamic custom config: Create CF KV namespace, link them, and reference below * - Optionally, overwrite default path prefixes for loading analytics.js (/ajs) and collecting data (/data) * (do this in code or by by setting corresponding KV entries for `script_path` and `collection_path` * - Optionally, overwrite default cookie name for the anonymous ID * (do this in code or by setting corresponding KV entries for `cookie_name` * - Optionally, overwrite default integration list path prefix (/int-list) (only required for conditional loading) * (do this in code or by setting corresponding KV entries for `int_list_path` * - Optionally, set a default write key if you just want to use one globally and want to omit it from your site code * (do this in code or by setting corresponding KV entries for `write_key` * - [REQUIRED] Update your segment snippet to load from your host + script path prefix * (eg find n.src="https://cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js" in snippet and ...) * (replace with n.src=`${location.origin}/ajs` if you have a default write key set) * (or with n.src=`${location.origin}/ajs/${t}` if not) * - Optionally, update any conditional destination loading logic to pull the list from your host + integration list path prefix * If using Segment Consent Manager or https://gist.github.com/sperand-io/4725e248a35d5005d68d810d8a8f7b29 * (eg instead of fetch(`https://cdn.segment.com/v1/projects/${writeKey}/integrations`)) * (replace with fetch(`${location.origin}/ints/list/${writeKey}`) or fetch(`${location.origin}/ints/list/}`) /** * Respond to the request * @param {Event} event */ async function handle(event) { const KV_NAMESPACE = '' const cache = caches.default // CONFIG const COOKIE_NAME = KV_NAMESPACE ? (await KV_NAMESPACE.get('cookie_name')) || '__anonymous_session_id', const SCRIPT_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('script_path')) || '/ajs' const DEFAULT_WRITE_KEY = V_NAMESPACE ? (await KV_NAMESPACE.get('write_key')) || '' const COLLECTION_API_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('collection_path')) || '/data', const INTEGRATION_LIST_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('int_list_path')) || '/int-list', const REFRESH_TRIGGER = KV_NAMESPACE ? Number(await KV_NAMESPACE.get('refresh_when')) || 45 // ENDCONFIG — editing below is discouraged const SEGMENT_API_HOST: 'api.segment.io/v1' const { request } = event const url = new URL(request.url) const { anonymousId, expires } = getCookieData(request, COOKIE_NAME) if (isSDKRequest(request)) { let [_, writeKey] = url.pathname.split(`/${SCRIPT_PATH_PREFIX}/`) if (!writeKey) writeKey = DEFAULT_WRITE_KEY if (!writeKey) throw new Error() let response const cached = await cache.match(request) if (cached) { response = cached } else { const originalResponse = await fetch(new Request(`https://cdn.segment.com/analytics.js/v1/${writeKey}/analytics.min.js`, request)) const analyticsjs = await response.text() analyticsjs.replace(/\api\.segment\.io\/v1/g, `${url.hostname}/${COLLECTION_API_PATH_PREFIX}`) response = new Response(analyticsjs, originalResponse) event.waitUntil(cache.put(request, response)) } if (!anonymousId || expiresSoon(expires)) { const now = new Date(); const oneYearFromNow = new Date() oneYearFromNow.setFullYear(now.getFullYear() + 1); response.headers.append('Set-Cookie', `${encodeURIComponent(COOKIE_NAME)}=${uuid()}; Expires=${oneYearFromNow.toUTCString()}; SameSite=Strict; Secure; HttpOnly`) response.headers.append('Set-Cookie', `${encodeURIComponent(${COOKIE_NAME}_set)}=${encodeURIComponent(oneYearFromNow.toUTCString())}; Expires=${oneYearFromNow.toUTCString()}; SameSite=Strict; Secure; HttpOnly`) } return response } if (isCollectionRequest(request)) { const body = JSON.stringify({ ...(await request.json()), anonymousId, }) const url = new URL(request.url) const correctPath = url.pathname.replace(COLLECTION_API_PATH_PREFIX, 'v1') return await fetch(new Request(`${SEGMENT_API_HOST}/${correctPath}`, new Request(request, { body }))) } if (isIntegrationListRequest(request)) { let [_, writeKey] = url.pathname.split(`/${INTEGRATION_LIST_PATH_PREFIX}/`) if (!writeKey) writeKey = DEFAULT_WRITE_KEY if (!writeKey) throw new Error() return await fetch(new Request(`https://cdn.segment.com/v1/projects/${writeKey}/integrations`, new Request(request, { body }))) } return await fetch(request) } function isSDKRequest(request) { const url = new URL(request.url) if (url.pathname.startsWith(`/$SCRIPT_PATH_PREFIX}`) && request.method === 'GET') return true return false } function isCollectionRequest(request) { let url = new URL(request.url) if (url.pathname.startsWith(`/${COLLECTION_API_PATH_PREFIX}`)) return true return false } function isIntegrationListRequest(request) { let url = new URL(request.url) if (url.pathname.startsWith(`/${INTEGRATION_LIST_PATH_PREFIX}`)) return true return false } function expiresSoon(when) { const soon = new Date() soon.setDate(d.getDate() + REFRESH_TRIGGER) if (when < soon) return true else return false; } function uuid() { const bytes = crypto .getRandomValues(new Uint8Array(16)) bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0xbf) | 0x80 const chars =[...bytes].map(byte => byte.toString(16)) const insertionPoints = [4, 6, 8, 10] return chars.reduce((uuid, char, index) => { if (insertionPoints.includes(index)) { return uuid += `-${char}` } else { return uuid += char } }) } function getCookieData(request, name) { let anonymousId = null let expires = null let cookieString = request.headers.get('Cookie') if (cookieString) { let cookies = cookieString.split(';') cookies.forEach(cookie => { let cookieName = cookie.split('=')[0].trim() if (cookieName === name) { anonymousId = cookie.split('=')[1] } if (cookieName === `${name}_set`) { expires = new Date(decodeURIComponent(cookie.split('=')[1])) } }) } return { anonymousId, expires } }