-
-
Save vunb/51b76988aa554adf6283ecf7c60aeaa3 to your computer and use it in GitHub Desktop.
Cloudflare Workers / Segment Smart Proxy — serve data collection assets and endpoints from your own domain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment