Skip to content

Instantly share code, notes, and snippets.

@vunb
Forked from sperand-io/worker.js
Created June 20, 2023 01:24
Show Gist options
  • Select an option

  • Save vunb/51b76988aa554adf6283ecf7c60aeaa3 to your computer and use it in GitHub Desktop.

Select an option

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
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