-
-
Save vunb/51b76988aa554adf6283ecf7c60aeaa3 to your computer and use it in GitHub Desktop.
Revisions
-
sperand-io revised this gist
Feb 24, 2023 . 1 changed file with 1 addition and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -163,8 +163,7 @@ async function handleScript( const originalResponse = await fetch(new Request(endpoint, request)) const newResponse = originalResponse.clone() const analyticsjs = await originalResponse.text() const modifiedAnalyticsjs = analyticsjs.replace( /\api\.segment\.io\/v1/g, `${hostname}/${COLLECTION_API_PATH_PREFIX}` ) -
sperand-io revised this gist
May 9, 2022 . No changes.There are no files selected for viewing
-
sperand-io revised this gist
Sep 29, 2021 . 1 changed file with 2 additions and 9 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -221,14 +221,8 @@ async function handleDataCollection( ) newRequest.headers.append('origin', `https://${hostname}`) return await fetch(newRequest) } /** @@ -295,7 +289,6 @@ async function hydrateConfig(KV) { return config }, STATIC_CONFIG) } /** -
sperand-io revised this gist
Jan 6, 2020 . 1 changed file with 6 additions and 5 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -16,7 +16,7 @@ * - You can overwrite the default refresh trigger if you want to more regularly update the anonymousId * (corresponding KV entry: `refresh_threshold`) * - You can set a path for echoing the session ID * (corresponding KV entry: `default_write_key`) * - You can set a default write key if you just want to use one globally and want to omit it from your site code * (corresponding KV entry: `write_key`) * - You can set an error collection endpoint if you have a logging service that accepts webhooks @@ -41,7 +41,7 @@ const STATIC_CONFIG = { SCRIPT_PATH_PREFIX: 'ajs', COLLECTION_API_PATH_PREFIX: 'data', INTEGRATION_LIST_PATH_PREFIX: 'ilist', ANONYMOUS_ID_ECHO_PATH: '', REFRESH_THRESHOLD: 45, DEFAULT_WRITE_KEY: '3K4xZlUgQFAa3MRdnRRKvbvDEukDCWeu', ERROR_ENDPOINT: 'https://enj0zt42hq1y.x.pipedream.net' @@ -105,9 +105,6 @@ async function handleEvent(event) { if (startsWith(url, SCRIPT_PATH_PREFIX)) return await handleScript(event, cache, cookieData, config) // serve first party data collection pings if (startsWith(url, COLLECTION_API_PATH_PREFIX)) return await handleDataCollection(request, cookieData, config) @@ -116,6 +113,10 @@ async function handleEvent(event) { if (startsWith(url, INTEGRATION_LIST_PATH_PREFIX)) return await handleIntegrationListing(request, config) // serve anonymousId echo if (ANONYMOUS_ID_ECHO_PATH && startsWith(url, ANONYMOUS_ID_ECHO_PATH)) return await handleEcho(event, cookieData) // passthrough everything else return await fetch(request) } -
sperand-io revised this gist
Jan 4, 2020 . 1 changed file with 34 additions and 8 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,6 +1,6 @@ /** * Steps to use: * 1. Create CF Worker, copy and paste this in * 2. (Optional) Update configuration defaults * - If you want to manage in code, do so below under "Static Configuration" * - If you want dynamic custom config: Create CFW KV namespace, link them, and add reference below @@ -15,10 +15,13 @@ * (corresponding KV entry: `integration_list_path_prefix`) * - You can overwrite the default refresh trigger if you want to more regularly update the anonymousId * (corresponding KV entry: `refresh_threshold`) * - You can set a path for echoing the session ID * (corresponding KV entry: `write_key`) * - You can set a default write key if you just want to use one globally and want to omit it from your site code * (corresponding KV entry: `write_key`) * - You can set an error collection endpoint if you have a logging service that accepts webhooks * (corresponding KV entry: `write_key`) * * 3. (If needed) If you use it for Consent Management, update any conditional destination loading logic to pull the integration list from your host + integration list path prefix * eg. If using Segment Consent Manager or https://gist.github.com/sperand-io/4725e248a35d5005d68d810d8a8f7b29 * ...instead of fetch(`https://cdn.segment.com/v1/projects/${writeKey}/integrations`) @@ -38,17 +41,18 @@ const STATIC_CONFIG = { SCRIPT_PATH_PREFIX: 'ajs', COLLECTION_API_PATH_PREFIX: 'data', INTEGRATION_LIST_PATH_PREFIX: 'ilist', ANONYMOUS_ID_ECHO_PATH: 'anonymousId', REFRESH_THRESHOLD: 45, DEFAULT_WRITE_KEY: '3K4xZlUgQFAa3MRdnRRKvbvDEukDCWeu', ERROR_ENDPOINT: 'https://enj0zt42hq1y.x.pipedream.net' } // END STATIC CONFIGUATION. Editing below this line is discouraged. /** * Attach top-level responder. */ addEventListener('fetch', event => { event.respondWith(handleErr(event)) }) /** @@ -59,9 +63,10 @@ addEventListener('fetch', event => { * * @param {Event} event */ async function handleErr(event) { try { const res = await handleEvent(event) return res } catch (err) { let endpoint = KV_NAMESPACE && (await KV_NAMESPACE.get('error_endpoint')) if (!endpoint) endpoint = STATIC_CONFIG['ERROR_ENDPOINT'] @@ -85,7 +90,8 @@ async function handleEvent(event) { COOKIE_NAME, SCRIPT_PATH_PREFIX, COLLECTION_API_PATH_PREFIX, INTEGRATION_LIST_PATH_PREFIX, ANONYMOUS_ID_ECHO_PATH } = config const cache = caches.default @@ -99,6 +105,9 @@ async function handleEvent(event) { if (startsWith(url, SCRIPT_PATH_PREFIX)) return await handleScript(event, cache, cookieData, config) // serve anonymousId echo if (startsWith(url, ANONYMOUS_ID_ECHO_PATH)) return await handleEcho(event, cookieData) // serve first party data collection pings if (startsWith(url, COLLECTION_API_PATH_PREFIX)) return await handleDataCollection(request, cookieData, config) @@ -240,6 +249,23 @@ async function handleIntegrationListing( return await fetch(new Request(endpoint, new Request(request, { body }))) } /** * Serve first party anonymousID echo API * * @param {Request} request * @param {Object} config */ async function handleEcho(request, { anonymousId }) { if (anonymousId) { return new Response(JSON.stringify({ anonymousId }), { headers: new Headers({ 'Content-Type': 'application/json' }) }) } new Response('No AnonymousId', { status: 404 }) } /** * HELPERS */ @@ -390,7 +416,7 @@ function errToJson(err, request) { key => !['name', 'message', 'stack'].includes(key) ) return { message: errType + ': ' + (err.message || '<no message>'), exception: { values: [ { -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 120 additions and 121 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -30,26 +30,26 @@ * (or with n.src=`${location.origin}/ajs/${t}` if not) */ let KV_NAMESPACE // START STATIC CONFIGURATION const STATIC_CONFIG = { COOKIE_NAME: '__anonymous_session_id', SCRIPT_PATH_PREFIX: 'ajs', COLLECTION_API_PATH_PREFIX: 'data', INTEGRATION_LIST_PATH_PREFIX: 'ilist', REFRESH_THRESHOLD: 45, DEFAULT_WRITE_KEY: '', ERROR_ENDPOINT: '' } // END STATIC CONFIGUATION. Editing below this line is discouraged. /** * Attach top-level responder. */ addEventListener('fetch', event => { event.respondWith(serve(event)) }) /** * Top level event handler. @@ -59,17 +59,16 @@ addEventListener("fetch", event => { * * @param {Event} event */ async function serve(event) { try { return await handleEvent(event) } catch (err) { let endpoint = KV_NAMESPACE && (await KV_NAMESPACE.get('error_endpoint')) if (!endpoint) endpoint = STATIC_CONFIG['ERROR_ENDPOINT'] if (endpoint) event.waitUntil(log(endpoint, err, event.request)) return new Response(err.message || 'An error occurred!', { status: err.statusCode || 500 }) } } @@ -81,35 +80,35 @@ async function handleErr(event) { * @param {Event} event */ async function handleEvent(event) { const config = KV_NAMESPACE ? await hydrateConfig(KV_NAMESPACE) : STATIC_CONFIG const { COOKIE_NAME, SCRIPT_PATH_PREFIX, COLLECTION_API_PATH_PREFIX, INTEGRATION_LIST_PATH_PREFIX } = config const cache = caches.default const { request } = event const url = new URL(request.url) // extract cookie information const cookieData = getCookieData(request, COOKIE_NAME) // serve analytics.js if (startsWith(url, SCRIPT_PATH_PREFIX)) return await handleScript(event, cache, cookieData, config) // serve first party data collection pings if (startsWith(url, COLLECTION_API_PATH_PREFIX)) return await handleDataCollection(request, cookieData, config) // serve first party data collection pings if (startsWith(url, INTEGRATION_LIST_PATH_PREFIX)) return await handleIntegrationListing(request, config) // passthrough everything else return await fetch(request) } /** @@ -140,44 +139,44 @@ async function handleScript( REFRESH_THRESHOLD } ) { const { request } = event const { pathname, hostname } = new URL(request.url) let [_, writeKey] = pathname.split(`/${SCRIPT_PATH_PREFIX}/`) if (!writeKey) writeKey = DEFAULT_WRITE_KEY let response const cached = await cache.match(request) if (cached) { response = cached } else { const endpoint = `https://cdn.segment.com/analytics.js/v1/${writeKey}/analytics.min.js` const originalResponse = await fetch(new Request(endpoint, request)) const newResponse = originalResponse.clone() const analyticsjs = await originalResponse.text() modifiedAnalyticsjs = analyticsjs.replace( /\api\.segment\.io\/v1/g, `${hostname}/${COLLECTION_API_PATH_PREFIX}` ) response = new Response(modifiedAnalyticsjs, newResponse) event.waitUntil(cache.put(request, response.clone())) } if (!anonymousId || expiresSoon(expires, REFRESH_THRESHOLD)) { const oneYearFromNow = new Date() oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1) response.headers.append( 'Set-Cookie', createCookie(COOKIE_NAME, uuid(), oneYearFromNow) ) response.headers.append( 'Set-Cookie', createCookie(`${COOKIE_NAME}_set`, oneYearFromNow.toUTCString(), oneYearFromNow) ) } return response } /** @@ -197,29 +196,29 @@ async function handleDataCollection( { anonymousId }, { COLLECTION_API_PATH_PREFIX } ) { const originalRequest = request.clone() const body = JSON.stringify({ ...(await request.json()), ...(anonymousId ? { anonymousId } : {}) }) const { pathname, hostname } = new URL(request.url) const correctPath = pathname.replace(COLLECTION_API_PATH_PREFIX, 'v1') const newRequest = new Request( `https://api.segment.io${correctPath}`, new Request(originalRequest, { body }) ) newRequest.headers.append('origin', `https://${hostname}`) const response = await fetch(newRequest) for (let [name, value] of response.headers.entries()) { console.log(name, value) } return response } /** @@ -234,11 +233,11 @@ async function handleIntegrationListing( request, { INTEGRATION_LIST_PATH_PREFIX, DEFAULT_WRITE_KEY } ) { const { pathname } = new URL(request.url) let [_, writeKey] = pathname.split(`/${INTEGRATION_LIST_PATH_PREFIX}/`) if (!writeKey) writeKey = DEFAULT_WRITE_KEY const endpoint = `https://cdn.segment.com/v1/projects/${writeKey}/integrations` return await fetch(new Request(endpoint, new Request(request, { body }))) } /** @@ -251,33 +250,33 @@ async function handleIntegrationListing( */ async function hydrateConfig(KV) { const keys = [ 'cookie_name', 'script_path_prefix', 'collection_api_path_prefix', 'integration_list_path_prefix', 'refresh_threshold', 'default_write_key' ] return Promise.all( keys.map(async k => { return { [k.toUpperCase()]: (await KV.get(k)) || '' } }) ).reduce((config, { key, storedKValue }) => { if (storedKValue) { config[key] = storedKValue } return config }, STATIC_CONFIG) console.log(config) } /** * Check if url path begins with a specified prefix */ function startsWith(url, prefix) { if (url.pathname.startsWith(`/${prefix}`)) return true return false } /** @@ -286,11 +285,11 @@ function startsWith(url, prefix) { */ function expiresSoon(when, REFRESH_THRESHOLD) { // eg. 45 days from now const threshold = new Date() threshold.setDate(threshold.getDate() + REFRESH_THRESHOLD) // is expiration in less than eg. 45 days? if (when < threshold) return true else return false } /** @@ -299,29 +298,29 @@ function expiresSoon(when, REFRESH_THRESHOLD) { function createCookie(name, value, expires) { return `${encodeURIComponent(name)}=${encodeURIComponent( value )}; Expires=${expires.toUTCString()}; SameSite=Strict; Secure; HttpOnly` } /** * Generate a spec-compliant uuid-v4 * adapted from: https://gist.github.com/bentranter/ed524091170137a72c1d54d641493c1f */ 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) } }) } /** @@ -333,22 +332,22 @@ function uuid() { * @param {string} name of the edge-side cookie */ 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 } } /** @@ -361,19 +360,19 @@ function getCookieData(request, name) { * @param {Request} request incoming Request */ async function log(endpoint, err, request) { const body = JSON.stringify(errToJson(err, request)) const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }) if (res.status === 200) { return } // We couldn't send to error endpoint, try to log the response at least console.error({ httpStatus: res.status, ...(await res.json()) }) // eslint-disable-line no-console } /** @@ -385,13 +384,13 @@ async function log(endpoint, err, request) { * @param {Request} request incoming Request */ function errToJson(err, request) { const errType = err.name || (err.contructor || {}).name const frames = parse(err) const extraKeys = Object.keys(err).filter( key => !['name', 'message', 'stack'].includes(key) ) return { message: `${errType}: ${err.message || '<no message>'}`, exception: { values: [ { @@ -406,7 +405,7 @@ function errToJson(err, request) { [errType]: extraKeys.reduce((obj, key) => ({ ...obj, [key]: err[key] }), {}) } : undefined, platform: 'worker', timestamp: Date.now() / 1000, request: request && request.url @@ -418,7 +417,7 @@ function errToJson(err, request) { data: request.body } : undefined } } /** @@ -429,29 +428,29 @@ function errToJson(err, request) { * @param {Error} err the error\ */ function parse(err) { return (err.stack || '') .split('\n') .slice(1) .map(line => { if (line.match(/^\s*[-]{4,}$/)) { return { filename: line } } // From https://github.com/felixge/node-stack-trace/blob/1ec9ba43eece124526c273c917104b4226898932/lib/stack-trace.js#L42 const lineMatch = line.match( /at (?:(.+)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/ ) if (!lineMatch) { return } return { function: lineMatch[1] || undefined, filename: lineMatch[2] || undefined, lineno: +lineMatch[3] || undefined, colno: +lineMatch[4] || undefined, in_app: lineMatch[5] !== 'native' || undefined } }) .filter(Boolean) } -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,6 +1,6 @@ /** * Steps to use: * 1. Create CF Worker & paste this in * 2. (Optional) Update configuration defaults * - If you want to manage in code, do so below under "Static Configuration" * - If you want dynamic custom config: Create CFW KV namespace, link them, and add reference below -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 144 additions and 30 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,39 +1,48 @@ /** * Steps to use: * 1. Create CF Worker, copy and paste this in * 2. (Optional) Update configuration defaults * - If you want to manage in code, do so below under "Static Configuration" * - If you want dynamic custom config: Create CFW KV namespace, link them, and add reference below * * - You can overwrite default path prefix for loading analytics.js (<yourdomain>/ajs) * (corresponding KV entry: `script_path_prefix`) * - You can overwrite default path prefix for handling first-party data collection (<yourdomain>/data) * (corresponding KV entry: `collection_api_path_prefix`) * - You can overwrite default cookie name for the edge-side anonymous ID * (corresponding KV entry: `cookie_name`) * - You can overwrite default integration list path prefix (/int-list) * (corresponding KV entry: `integration_list_path_prefix`) * - You can overwrite the default refresh trigger if you want to more regularly update the anonymousId * (corresponding KV entry: `refresh_threshold`) * - You can set a default write key if you just want to use one globally and want to omit it from your site code * (corresponding KV entry: `write_key`) * - You can set an error collection endpoint if you have a logging service that accepts webhooks * (corresponding KV entry: `write_key`) * 3. (If needed) If you use it for Consent Management, update any conditional destination loading logic to pull the integration list from your host + integration list path prefix * eg. If using Segment Consent Manager or https://gist.github.com/sperand-io/4725e248a35d5005d68d810d8a8f7b29 * ...instead of fetch(`https://cdn.segment.com/v1/projects/${writeKey}/integrations`) * ...replace with fetch(`${location.origin}/ilist/${writeKey}`) or fetch(`${location.origin}/ilist/}`) * 3. (REQUIRED) Deploy and configure the worker to serve for your desired domain/subdomain and at your desired path * 4. (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) */ let KV_NAMESPACE; // START STATIC CONFIGURATION const STATIC_CONFIG = { COOKIE_NAME: "__anonymous_session_id", SCRIPT_PATH_PREFIX: "ajs", COLLECTION_API_PATH_PREFIX: "data", INTEGRATION_LIST_PATH_PREFIX: "ilist", REFRESH_THRESHOLD: 45, DEFAULT_WRITE_KEY: "3K4xZlUgQFAa3MRdnRRKvbvDEukDCWeu", ERROR_ENDPOINT: "https://enj0zt42hq1y.x.pipedream.net" }; // END STATIC CONFIGUATION. Editing below this line is discouraged. /** * Attach top-level responder. @@ -66,6 +75,9 @@ async function handleErr(event) { /** * Respond to the request * * Provides special handling for Segment requests against the configured || default paths. * * @param {Event} event */ async function handleEvent(event) { @@ -80,17 +92,42 @@ async function handleEvent(event) { const { request } = event; const url = new URL(request.url); // extract cookie information const cookieData = getCookieData(request, COOKIE_NAME); // serve analytics.js if (startsWith(url, SCRIPT_PATH_PREFIX)) return await handleScript(event, cache, cookieData, config); // serve first party data collection pings if (startsWith(url, COLLECTION_API_PATH_PREFIX)) return await handleDataCollection(request, cookieData, config); // serve first party data collection pings if (startsWith(url, INTEGRATION_LIST_PATH_PREFIX)) return await handleIntegrationListing(request, config); // passthrough everything else return await fetch(request); } /** * Serve analytics.js * * Serves a modified analytics.js for (default || passed) writeKey at (default || configured) (path || path prefix) * Mods: * If writeKey is omitted, get the default script * Updates data collection api host in the script itself * If needed, sets an HTTPOnly anonymous session cookie (and corresponding set-at cookie) * * @param {Event} event * @param {Cache} cache * @param {Object} cookieData * @param {String} cookieData.anonymousId * @param {Date} cookieData.expires * @param {Object} config */ async function handleScript( event, cache, @@ -100,7 +137,7 @@ async function handleScript( DEFAULT_WRITE_KEY, COLLECTION_API_PATH_PREFIX, COOKIE_NAME, REFRESH_THRESHOLD } ) { const { request } = event; @@ -127,7 +164,7 @@ async function handleScript( event.waitUntil(cache.put(request, response.clone())); } if (!anonymousId || expiresSoon(expires, REFRESH_THRESHOLD)) { const oneYearFromNow = new Date(); oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); response.headers.append( @@ -143,6 +180,18 @@ async function handleScript( return response; } /** * Serve first party data collection API * * Serves a handler to modify and forward events to Segment at the default || configured path prefix * Mods: * If present in the request cookie, overwrites anonymousId with edge-side cookie value * * @param {Request} request * @param {Object} cookieData * @param {String} cookieData.anonymousId * @param {Object} config */ async function handleDataCollection( request, { anonymousId }, @@ -173,6 +222,14 @@ async function handleDataCollection( return response; } /** * Serve first party integration list API * * Serves a handler to passthrough list requests for default || passed writeKey at the default || configured path prefix * * @param {Request} request * @param {Object} config */ async function handleIntegrationListing( request, { INTEGRATION_LIST_PATH_PREFIX, DEFAULT_WRITE_KEY } @@ -184,18 +241,26 @@ async function handleIntegrationListing( return await fetch(new Request(endpoint, new Request(request, { body }))); } /** * HELPERS */ /** * Check if url path begins with a specified prefix * @param {NAMESPACE} KV */ async function hydrateConfig(KV) { const keys = [ "cookie_name", "script_path_prefix", "collection_api_path_prefix", "integration_list_path_prefix", "refresh_threshold", "default_write_key" ]; return Promise.all( keys.map(async k => { return { [k.toUpperCase()]: (await KV.get(k)) || "" }; }) ).reduce((config, { key, storedKValue }) => { if (storedKValue) { @@ -207,24 +272,40 @@ async function hydrateConfig(ns) { console.log(config); } /** * Check if url path begins with a specified prefix */ function startsWith(url, prefix) { if (url.pathname.startsWith(`/${prefix}`)) return true; return false; } /** * Check if the anonymousId is due to be refreshed * (ie. is our expiration closer than our threshold window allows?) */ function expiresSoon(when, REFRESH_THRESHOLD) { // eg. 45 days from now const threshold = new Date(); threshold.setDate(threshold.getDate() + REFRESH_THRESHOLD); // is expiration in less than eg. 45 days? if (when < threshold) return true; else return false; } /** * Encode a cookie string suited for our use case */ function createCookie(name, value, expires) { return `${encodeURIComponent(name)}=${encodeURIComponent( value )}; Expires=${expires.toUTCString()}; SameSite=Strict; Secure; HttpOnly`; } /** * Generate a spec-compliant uuid-v4 * adapted from: https://gist.github.com/bentranter/ed524091170137a72c1d54d641493c1f */ function uuid() { const bytes = crypto.getRandomValues(new Uint8Array(16)); @@ -243,6 +324,14 @@ function uuid() { }); } /** * Grabs the anonymousId and expiration time from the cookies in the request header * * Adapted from: https://developers.cloudflare.com/workers/templates/pages/cookie_extract/ * * @param {Request} request incoming Request * @param {string} name of the edge-side cookie */ function getCookieData(request, name) { let anonymousId = null; let expires = null; @@ -262,6 +351,15 @@ function getCookieData(request, name) { return { anonymousId, expires }; } /** * Ship the error with some helpful request context as JSON to the specified endpoint * * ADAPTED from https://github.com/bustle/cf-sentry/ * * @param {String} endpoint * @param {Error} err the error * @param {Request} request incoming Request */ async function log(endpoint, err, request) { const body = JSON.stringify(errToJson(err, request)); const res = await fetch(endpoint, { @@ -278,6 +376,14 @@ async function log(endpoint, err, request) { console.error({ httpStatus: res.status, ...(await res.json()) }); // eslint-disable-line no-console } /** * Encode the parsed and formatted error as JSON * * ADAPTED from https://github.com/bustle/cf-sentry/ * * @param {Error} err the error * @param {Request} request incoming Request */ function errToJson(err, request) { const errType = err.name || (err.contructor || {}).name; const frames = parse(err); @@ -300,7 +406,7 @@ function errToJson(err, request) { [errType]: extraKeys.reduce((obj, key) => ({ ...obj, [key]: err[key] }), {}) } : undefined, platform: "worker", timestamp: Date.now() / 1000, request: request && request.url @@ -315,6 +421,13 @@ function errToJson(err, request) { }; } /** * Parse errors. * * ADAPTED from https://github.com/bustle/cf-sentry/ * * @param {Error} err the error\ */ function parse(err) { return (err.stack || "") .split("\n") @@ -324,6 +437,7 @@ function parse(err) { return { filename: line }; } // From https://github.com/felixge/node-stack-trace/blob/1ec9ba43eece124526c273c917104b4226898932/lib/stack-trace.js#L42 const lineMatch = line.match( /at (?:(.+)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/ ); -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 4 additions and 4 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -10,14 +10,14 @@ * (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` * - 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}/ilist/${writeKey}`) or fetch(`${location.origin}/ilist/}`) * - [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) */ // START STATIC CONFIGURATION -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 241 additions and 99 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -20,139 +20,201 @@ * (replace with fetch(`${location.origin}/ilist/${writeKey}`) or fetch(`${location.origin}/ilist/}`) */ // START STATIC CONFIGURATION // recommendation is to not touch these, and instead override these values with workers KV // if you want to edit them in code, just change the values after the `:` const KV_NAMESPACE = ""; const STATIC_CONFIG = { COOKIE_NAME: "__anonymous_session_id", SCRIPT_PATH_PREFIX: "ajs", COLLECTION_API_PATH_PREFIX: "data", INTEGRATION_LIST_PATH_PREFIX: "ilist", REFRESH_TRIGGER: 45, DEFAULT_WRITE_KEY: "3K4xZlUgQFAa3MRdnRRKvbvDEukDCWeu", ERROR_ENDPOINT: "https://enj0zt42hq1y.x.pipedream.net" }; // END Config. Editing below this line is discouraged. /** * Attach top-level responder. */ addEventListener("fetch", event => { event.respondWith(handleErr(event)); }); /** * Top level event handler. * * Wraps our request handler in an error handler, * optionally forward errors to a logging service. * * @param {Event} event */ async function handleErr(event) { try { const res = await handleEvent(event); return res; } catch (err) { let endpoint = KV_NAMESPACE && (await KV_NAMESPACE.get("error_endpoint")); if (!endpoint) endpoint = STATIC_CONFIG["ERROR_ENDPOINT"]; if (endpoint) event.waitUntil(log(endpoint, err, event.request)); return new Response(err.message || "An error occurred!", { status: err.statusCode || 500 }); } } /** * Respond to the request * @param {Event} event */ async function handleEvent(event) { const config = KV_NAMESPACE ? await hydrateConfig(KV_NAMESPACE) : STATIC_CONFIG; const { COOKIE_NAME, SCRIPT_PATH_PREFIX, COLLECTION_API_PATH_PREFIX, INTEGRATION_LIST_PATH_PREFIX } = config; const cache = caches.default; const { request } = event; const url = new URL(request.url); const cookieData = getCookieData(request, COOKIE_NAME); if (startsWith(url, SCRIPT_PATH_PREFIX)) return await handleScript(event, cache, cookieData, config); if (startsWith(url, COLLECTION_API_PATH_PREFIX)) return await handleDataCollection(request, cookieData, config); if (startsWith(url, INTEGRATION_LIST_PATH_PREFIX)) return await handleIntegrationListing(request, config); return await fetch(request); } async function handleScript( event, cache, { anonymousId, expires }, { SCRIPT_PATH_PREFIX, DEFAULT_WRITE_KEY, COLLECTION_API_PATH_PREFIX, COOKIE_NAME, REFRESH_TRIGGER } ) { const { request } = event; const { pathname, hostname } = new URL(request.url); let [_, writeKey] = pathname.split(`/${SCRIPT_PATH_PREFIX}/`); if (!writeKey) writeKey = DEFAULT_WRITE_KEY; let response; const cached = await cache.match(request); if (cached) { response = cached; } else { const endpoint = `https://cdn.segment.com/analytics.js/v1/${writeKey}/analytics.min.js`; const originalResponse = await fetch(new Request(endpoint, request)); const newResponse = originalResponse.clone(); const analyticsjs = await originalResponse.text(); modifiedAnalyticsjs = analyticsjs.replace( /\api\.segment\.io\/v1/g, `${hostname}/${COLLECTION_API_PATH_PREFIX}` ); response = new Response(modifiedAnalyticsjs, newResponse); event.waitUntil(cache.put(request, response.clone())); } if (!anonymousId || expiresSoon(expires, REFRESH_TRIGGER)) { const oneYearFromNow = new Date(); oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); response.headers.append( "Set-Cookie", createCookie(COOKIE_NAME, uuid(), oneYearFromNow) ); response.headers.append( "Set-Cookie", createCookie(`${COOKIE_NAME}_set`, oneYearFromNow.toUTCString(), oneYearFromNow) ); } return response; } async function handleDataCollection( request, { anonymousId }, { COLLECTION_API_PATH_PREFIX } ) { const originalRequest = request.clone(); const body = JSON.stringify({ ...(await request.json()), ...(anonymousId ? { anonymousId } : {}) }); const { pathname, hostname } = new URL(request.url); const correctPath = pathname.replace(COLLECTION_API_PATH_PREFIX, "v1"); const newRequest = new Request( `https://api.segment.io${correctPath}`, new Request(originalRequest, { body }) ); newRequest.headers.append("origin", `https://${hostname}`); const response = await fetch(newRequest); for (let [name, value] of response.headers.entries()) { console.log(name, value); } return response; } async function handleIntegrationListing( request, { INTEGRATION_LIST_PATH_PREFIX, DEFAULT_WRITE_KEY } ) { const { pathname } = new URL(request.url); let [_, writeKey] = pathname.split(`/${INTEGRATION_LIST_PATH_PREFIX}/`); if (!writeKey) writeKey = DEFAULT_WRITE_KEY; const endpoint = `https://cdn.segment.com/v1/projects/${writeKey}/integrations`; return await fetch(new Request(endpoint, new Request(request, { body }))); } async function hydrateConfig(ns) { const keys = [ "cookie_name", "script_path_prefix", "collection_api_path_prefix", "integration_list_path_prefix", "refresh_trigger", "default_write_key" ]; return Promise.all( keys.map(async k => { return { [k.toUpperCase()]: (await ns.get(k)) || "" }; }) ).reduce((config, { key, storedKValue }) => { if (storedKValue) { config[key] = storedKValue; } return config; }, STATIC_CONFIG); console.log(config); } function startsWith(url, prefix) { if (url.pathname.startsWith(`/${prefix}`)) return true; return false; } function expiresSoon(when, REFRESH_TRIGGER) { const soon = new Date(); soon.setDate(soon.getDate() + REFRESH_TRIGGER); if (when < soon) return true; else return false; } @@ -199,3 +261,83 @@ function getCookieData(request, name) { } return { anonymousId, expires }; } async function log(endpoint, err, request) { const body = JSON.stringify(errToJson(err, request)); const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body }); if (res.status === 200) { return; } // We couldn't send to error endpoint, try to log the response at least console.error({ httpStatus: res.status, ...(await res.json()) }); // eslint-disable-line no-console } function errToJson(err, request) { const errType = err.name || (err.contructor || {}).name; const frames = parse(err); const extraKeys = Object.keys(err).filter( key => !["name", "message", "stack"].includes(key) ); return { message: errType + ": " + (err.message || "<no message>"), exception: { values: [ { type: errType, value: err.message, stacktrace: frames.length ? { frames: frames.reverse() } : undefined } ] }, extra: extraKeys.length ? { [errType]: extraKeys.reduce((obj, key) => ({ ...obj, [key]: err[key] }), {}) } : undefined, platform: "javascript", timestamp: Date.now() / 1000, request: request && request.url ? { method: request.method, url: request.url, query_string: request.query, headers: request.headers, data: request.body } : undefined }; } function parse(err) { return (err.stack || "") .split("\n") .slice(1) .map(line => { if (line.match(/^\s*[-]{4,}$/)) { return { filename: line }; } const lineMatch = line.match( /at (?:(.+)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/ ); if (!lineMatch) { return; } return { function: lineMatch[1] || undefined, filename: lineMatch[2] || undefined, lineno: +lineMatch[3] || undefined, colno: +lineMatch[4] || undefined, in_app: lineMatch[5] !== "native" || undefined }; }) .filter(Boolean); } -
sperand-io revised this gist
Jan 3, 2020 . No changes.There are no files selected for viewing
-
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -54,6 +54,8 @@ async function handle(event) { ? await KV_NAMESPACE.get("default_write_key") : ""; const SEGMENT_API_HOST = "api.segment.io/v1"; // END Config. Editing below this line is discouraged. const { request } = event; const url = new URL(request.url); -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 141 additions and 106 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,164 +1,199 @@ /** * 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}/ilist/${writeKey}`) or fetch(`${location.origin}/ilist/}`) */ const KV_NAMESPACE = ""; addEventListener("fetch", event => { event.respondWith(handle(event)); }); /** * Respond to the request * @param {Event} event */ async function handle(event) { const cache = caches.default; // START CONFIG // recommendation is to override these values with workers KV // if you want to edit them in code, just change the values after the `:` 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_prefix") : "/ajs"; const COLLECTION_API_PATH_PREFIX = KV_NAMESPACE ? await KV_NAMESPACE.get("collection_api_path_prefix") : "/data"; const INTEGRATION_LIST_PATH_PREFIX = KV_NAMESPACE ? await KV_NAMESPACE.get("integration_list_path_prefix") : "/ilist"; const REFRESH_TRIGGER = KV_NAMESPACE ? Number(await KV_NAMESPACE.get("refresh_when")) : 45; const DEFAULT_WRITE_KEY = KV_NAMESPACE ? await KV_NAMESPACE.get("default_write_key") : ""; 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 (isScriptRequest(request)) { let [_, writeKey] = url.pathname.split(`/${SCRIPT_PATH_PREFIX}/`); if (!writeKey) writeKey = DEFAULT_WRITE_KEY; let response; const cached = await cache.match(request); if (cached) { response = cached; } else { const endpoint = `https://cdn.segment.com/analytics.js/v1/${writeKey}/analytics.min.js`; const originalResponse = await fetch(new Request(endpoint, 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 oneYearFromNow = new Date(); oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); response.headers.append( "Set-Cookie", createCookie(COOKIE_NAME, uuid(), oneYearFromNow) ); response.headers.append( "Set-Cookie", createCookie( `${COOKIE_NAME}_set`, oneYearFromNow.toUTCString(), oneYearFromNow ) ); } 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; const endpoint = `https://cdn.segment.com/v1/projects/${writeKey}/integrations`; return await fetch(new Request(endpoint, new Request(request, { body }))); } return await fetch(request); } function isScriptRequest(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 createCookie(name, value, expires) { return `${encodeURIComponent(name)}=${encodeURIComponent( value )}; Expires=${expires.toUTCString()}; SameSite=Strict; Secure; HttpOnly`; } 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 }; } -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 2 additions and 3 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -45,10 +45,9 @@ async function handle(event) { const { anonymousId, expires } = getCookieData(request, COOKIE_NAME) if (isScriptRequest(request)) { let [_, writeKey] = url.pathname.split(`/${SCRIPT_PATH_PREFIX}/`) if (!writeKey) writeKey = DEFAULT_WRITE_KEY let response const cached = await cache.match(request) @@ -96,7 +95,7 @@ async function handle(event) { return await fetch(request) } function isScriptRequest(request) { const url = new URL(request.url) if (url.pathname.startsWith(`/$SCRIPT_PATH_PREFIX}`) && request.method === 'GET') return true return false -
sperand-io revised this gist
Jan 3, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -38,7 +38,7 @@ async function handle(event) { const COLLECTION_API_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('collection_api_path_prefix')) || '/data' const INTEGRATION_LIST_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('integration_list_path_prefix')) || '/ilist' const REFRESH_TRIGGER = KV_NAMESPACE ? Number(await KV_NAMESPACE.get('refresh_when')) || 45 const SEGMENT_API_HOST = 'api.segment.io/v1' const { request } = event const url = new URL(request.url) -
sperand-io renamed this gist
Jan 3, 2020 . 1 changed file with 24 additions and 20 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,3 @@ /** * Steps to use: * - Create CF Worker, copy and paste this in @@ -21,30 +17,32 @@ addEventListener('fetch', event => { * - 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}/ilist/${writeKey}`) or fetch(`${location.origin}/ilist/}`) */ const KV_NAMESPACE = '' addEventListener('fetch', event => { event.respondWith(handle(event)) }) /** * Respond to the request * @param {Event} event */ async function handle(event) { const cache = caches.default 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_prefix')) || '/ajs' const DEFAULT_WRITE_KEY = KV_NAMESPACE ? (await KV_NAMESPACE.get('default_write_key')) || '' const COLLECTION_API_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('collection_api_path_prefix')) || '/data' const INTEGRATION_LIST_PATH_PREFIX = KV_NAMESPACE ? (await KV_NAMESPACE.get('integration_list_path_prefix')) || '/ilist' const REFRESH_TRIGGER = KV_NAMESPACE ? Number(await KV_NAMESPACE.get('refresh_when')) || 45 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)) { @@ -57,7 +55,8 @@ async function handle(event) { if (cached) { response = cached } else { const endpoint = `https://cdn.segment.com/analytics.js/v1/${writeKey}/analytics.min.js` const originalResponse = await fetch(new Request(endpoint, request)) const analyticsjs = await response.text() analyticsjs.replace(/\api\.segment\.io\/v1/g, `${url.hostname}/${COLLECTION_API_PATH_PREFIX}`) response = new Response(analyticsjs, originalResponse) @@ -68,7 +67,8 @@ async function handle(event) { const now = new Date(); const oneYearFromNow = new Date() oneYearFromNow.setFullYear(now.getFullYear() + 1); response.headers.append('Set-Cookie', createCookie(COOKIE_NAME, uuid(), oneYearFromNow)) response.headers.append('Set-Cookie', `${encodeURIComponent(${COOKIE_NAME}_set)}=${encodeURIComponent(oneYearFromNow.toUTCString())}; Expires=${oneYearFromNow.toUTCString()}; SameSite=Strict; Secure; HttpOnly`) } @@ -89,8 +89,8 @@ async function handle(event) { if (isIntegrationListRequest(request)) { let [_, writeKey] = url.pathname.split(`/${INTEGRATION_LIST_PATH_PREFIX}/`) if (!writeKey) writeKey = DEFAULT_WRITE_KEY const endpoint = `https://cdn.segment.com/v1/projects/${writeKey}/integrations` return await fetch(new Request(endpoint, new Request(request, { body }))) } return await fetch(request) @@ -121,6 +121,10 @@ function expiresSoon(when) { else return false; } function createCookie(name, value, expires) { return `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Expires=${expires.toUTCString()}; SameSite=Strict; Secure; HttpOnly` } function uuid() { const bytes = crypto .getRandomValues(new Uint8Array(16)) -
sperand-io revised this gist
Jan 3, 2020 . No changes.There are no files selected for viewing
-
sperand-io created this gist
Jan 3, 2020 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,161 @@ 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 } }