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
  • Save vunb/51b76988aa554adf6283ecf7c60aeaa3 to your computer and use it in GitHub Desktop.
Save vunb/51b76988aa554adf6283ecf7c60aeaa3 to your computer and use it in GitHub Desktop.

Revisions

  1. @sperand-io sperand-io revised this gist Feb 24, 2023. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions worker.js
    Original 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()

    modifiedAnalyticsjs = analyticsjs.replace(
    const modifiedAnalyticsjs = analyticsjs.replace(
    /\api\.segment\.io\/v1/g,
    `${hostname}/${COLLECTION_API_PATH_PREFIX}`
    )
  2. @sperand-io sperand-io revised this gist May 9, 2022. No changes.
  3. @sperand-io sperand-io revised this gist Sep 29, 2021. 1 changed file with 2 additions and 9 deletions.
    11 changes: 2 additions & 9 deletions worker.js
    Original file line number Diff line number Diff line change
    @@ -221,14 +221,8 @@ async function handleDataCollection(
    )

    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

    return await fetch(newRequest)
    }

    /**
    @@ -295,7 +289,6 @@ async function hydrateConfig(KV) {
    return config
    }, STATIC_CONFIG)

    console.log(config)
    }

    /**
  4. @sperand-io sperand-io revised this gist Jan 6, 2020. 1 changed file with 6 additions and 5 deletions.
    11 changes: 6 additions & 5 deletions worker.js
    Original 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: `write_key`)
    * (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: 'anonymousId',
    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 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)
    @@ -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)
    }
  5. @sperand-io sperand-io revised this gist Jan 4, 2020. 1 changed file with 34 additions and 8 deletions.
    42 changes: 34 additions & 8 deletions worker.js
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    /**
    * Steps to use:
    * 1. Create CF Worker & paste this in
    * 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: '',
    ERROR_ENDPOINT: ''
    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(serve(event))
    event.respondWith(handleErr(event))
    })

    /**
    @@ -59,9 +63,10 @@ addEventListener('fetch', event => {
    *
    * @param {Event} event
    */
    async function serve(event) {
    async function handleErr(event) {
    try {
    return await handleEvent(event)
    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
    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>'}`,
    message: errType + ': ' + (err.message || '<no message>'),
    exception: {
    values: [
    {
  6. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 120 additions and 121 deletions.
    241 changes: 120 additions & 121 deletions worker.js
    Original 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;
    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",
    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"
    };
    DEFAULT_WRITE_KEY: '',
    ERROR_ENDPOINT: ''
    }
    // END STATIC CONFIGUATION. Editing below this line is discouraged.

    /**
    * Attach top-level responder.
    */
    addEventListener("fetch", event => {
    event.respondWith(handleErr(event));
    });
    addEventListener('fetch', event => {
    event.respondWith(serve(event))
    })

    /**
    * Top level event handler.
    @@ -59,17 +59,16 @@ addEventListener("fetch", event => {
    *
    * @param {Event} event
    */
    async function handleErr(event) {
    async function serve(event) {
    try {
    const res = await handleEvent(event);
    return res;
    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!", {
    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 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;
    } = config
    const cache = caches.default

    const { request } = event;
    const url = new URL(request.url);
    const { request } = event
    const url = new URL(request.url)

    // extract cookie information
    const cookieData = getCookieData(request, COOKIE_NAME);
    const cookieData = getCookieData(request, COOKIE_NAME)

    // serve analytics.js
    if (startsWith(url, SCRIPT_PATH_PREFIX))
    return await handleScript(event, cache, cookieData, config);
    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);
    return await handleDataCollection(request, cookieData, config)

    // serve first party data collection pings
    if (startsWith(url, INTEGRATION_LIST_PATH_PREFIX))
    return await handleIntegrationListing(request, config);
    return await handleIntegrationListing(request, config)

    // passthrough everything else
    return await fetch(request);
    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);
    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;
    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();
    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()));
    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);
    const oneYearFromNow = new Date()
    oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
    response.headers.append(
    "Set-Cookie",
    'Set-Cookie',
    createCookie(COOKIE_NAME, uuid(), oneYearFromNow)
    );
    )
    response.headers.append(
    "Set-Cookie",
    'Set-Cookie',
    createCookie(`${COOKIE_NAME}_set`, oneYearFromNow.toUTCString(), oneYearFromNow)
    );
    )
    }

    return response;
    return response
    }

    /**
    @@ -197,29 +196,29 @@ async function handleDataCollection(
    { anonymousId },
    { COLLECTION_API_PATH_PREFIX }
    ) {
    const originalRequest = request.clone();
    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 { 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}`);
    newRequest.headers.append('origin', `https://${hostname}`)

    const response = await fetch(newRequest);
    const response = await fetch(newRequest)

    for (let [name, value] of response.headers.entries()) {
    console.log(name, value);
    console.log(name, value)
    }

    return response;
    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 })));
    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"
    ];
    '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)) || "" };
    return { [k.toUpperCase()]: (await KV.get(k)) || '' }
    })
    ).reduce((config, { key, storedKValue }) => {
    if (storedKValue) {
    config[key] = storedKValue;
    config[key] = storedKValue
    }
    return config;
    }, STATIC_CONFIG);
    return config
    }, STATIC_CONFIG)

    console.log(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;
    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);
    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;
    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`;
    )}; 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));
    const bytes = crypto.getRandomValues(new Uint8Array(16))

    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0xbf) | 0x80;
    bytes[6] = (bytes[6] & 0x0f) | 0x40
    bytes[8] = (bytes[8] & 0xbf) | 0x80

    const chars = [...bytes].map(byte => byte.toString(16));
    const chars = [...bytes].map(byte => byte.toString(16))

    const insertionPoints = [4, 6, 8, 10];
    const insertionPoints = [4, 6, 8, 10]
    return chars.reduce((uuid, char, index) => {
    if (insertionPoints.includes(index)) {
    return (uuid += `-${char}`);
    return (uuid += `-${char}`)
    } else {
    return (uuid += char);
    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");
    let anonymousId = null
    let expires = null
    let cookieString = request.headers.get('Cookie')
    if (cookieString) {
    let cookies = cookieString.split(";");
    let cookies = cookieString.split(';')
    cookies.forEach(cookie => {
    let cookieName = cookie.split("=")[0].trim();
    let cookieName = cookie.split('=')[0].trim()
    if (cookieName === name) {
    anonymousId = cookie.split("=")[1];
    anonymousId = cookie.split('=')[1]
    }
    if (cookieName === `${name}_set`) {
    expires = new Date(decodeURIComponent(cookie.split("=")[1]));
    expires = new Date(decodeURIComponent(cookie.split('=')[1]))
    }
    });
    })
    }
    return { anonymousId, expires };
    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 body = JSON.stringify(errToJson(err, request))
    const res = await fetch(endpoint, {
    method: "POST",
    method: 'POST',
    headers: {
    "Content-Type": "application/json"
    'Content-Type': 'application/json'
    },
    body
    });
    })
    if (res.status === 200) {
    return;
    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
    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 errType = err.name || (err.contructor || {}).name
    const frames = parse(err)
    const extraKeys = Object.keys(err).filter(
    key => !["name", "message", "stack"].includes(key)
    );
    key => !['name', 'message', 'stack'].includes(key)
    )
    return {
    message: errType + ": " + (err.message || "<no message>"),
    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",
    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")
    return (err.stack || '')
    .split('\n')
    .slice(1)
    .map(line => {
    if (line.match(/^\s*[-]{4,}$/)) {
    return { filename: line };
    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
    }

    return {
    function: lineMatch[1] || undefined,
    filename: lineMatch[2] || undefined,
    lineno: +lineMatch[3] || undefined,
    colno: +lineMatch[4] || undefined,
    in_app: lineMatch[5] !== "native" || undefined
    };
    in_app: lineMatch[5] !== 'native' || undefined
    }
    })
    .filter(Boolean);
    .filter(Boolean)
    }
  7. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion worker.js
    Original 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
    * 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
  8. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 144 additions and 30 deletions.
    174 changes: 144 additions & 30 deletions worker.js
    Original file line number Diff line number Diff line change
    @@ -1,39 +1,48 @@
    /**
    * 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`
    * - 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
    * 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
    // 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,
    REFRESH_THRESHOLD: 45,
    DEFAULT_WRITE_KEY: "3K4xZlUgQFAa3MRdnRRKvbvDEukDCWeu",
    ERROR_ENDPOINT: "https://enj0zt42hq1y.x.pipedream.net"
    };
    // END Config. Editing below this line is discouraged.
    // 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_TRIGGER
    REFRESH_THRESHOLD
    }
    ) {
    const { request } = event;
    @@ -127,7 +164,7 @@ async function handleScript(
    event.waitUntil(cache.put(request, response.clone()));
    }

    if (!anonymousId || expiresSoon(expires, REFRESH_TRIGGER)) {
    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 })));
    }

    async function hydrateConfig(ns) {
    /**
    * 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_trigger",
    "refresh_threshold",
    "default_write_key"
    ];
    return Promise.all(
    keys.map(async k => {
    return { [k.toUpperCase()]: (await ns.get(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;
    }

    function expiresSoon(when, REFRESH_TRIGGER) {
    const soon = new Date();
    soon.setDate(soon.getDate() + REFRESH_TRIGGER);
    if (when < soon) return true;
    /**
    * 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: "javascript",
    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+))?|([^)]+))\)?/
    );
  9. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions worker.js
    Original 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`
    * - [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/}`)
    * - [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
  10. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 241 additions and 99 deletions.
    340 changes: 241 additions & 99 deletions worker.js
    Original 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(handle(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 handle(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;

    // 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";

    // END Config. Editing below this line is discouraged.

    const { request } = event;
    const url = new URL(request.url);
    const cookieData = getCookieData(request, COOKIE_NAME);

    const { anonymousId, expires } = 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);
    }

    if (isScriptRequest(request)) {
    let [_, writeKey] = url.pathname.split(`/${SCRIPT_PATH_PREFIX}/`);
    if (!writeKey) writeKey = DEFAULT_WRITE_KEY;
    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);

    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 (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();

    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
    )
    );
    }
    modifiedAnalyticsjs = analyticsjs.replace(
    /\api\.segment\.io\/v1/g,
    `${hostname}/${COLLECTION_API_PATH_PREFIX}`
    );

    return response;
    response = new Response(modifiedAnalyticsjs, newResponse);
    event.waitUntil(cache.put(request, response.clone()));
    }

    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 (!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)
    );
    }

    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 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 await fetch(request);
    return response;
    }

    function isScriptRequest(request) {
    const url = new URL(request.url);
    if (
    url.pathname.startsWith(`/$SCRIPT_PATH_PREFIX}`) &&
    request.method === "GET"
    )
    return true;
    return false;
    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 })));
    }

    function isCollectionRequest(request) {
    let url = new URL(request.url);
    if (url.pathname.startsWith(`/${COLLECTION_API_PATH_PREFIX}`)) return true;
    return false;
    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 isIntegrationListRequest(request) {
    let url = new URL(request.url);
    if (url.pathname.startsWith(`/${INTEGRATION_LIST_PATH_PREFIX}`)) return true;
    function startsWith(url, prefix) {
    if (url.pathname.startsWith(`/${prefix}`)) return true;
    return false;
    }

    function expiresSoon(when) {
    function expiresSoon(when, REFRESH_TRIGGER) {
    const soon = new Date();
    soon.setDate(d.getDate() + REFRESH_TRIGGER);
    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);
    }
  11. @sperand-io sperand-io revised this gist Jan 3, 2020. No changes.
  12. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions worker.js
    Original 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);
  13. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 141 additions and 106 deletions.
    247 changes: 141 additions & 106 deletions worker.js
    Original 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))
    })
    * 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

    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)

    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)
    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
    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))
    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 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`)
    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
    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 })))
    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 })))
    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)
    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
    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
    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
    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
    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`
    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]
    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}`
    return (uuid += `-${char}`);
    } else {
    return uuid += char
    return (uuid += char);
    }
    })
    });
    }

    function getCookieData(request, name) {
    let anonymousId = null
    let expires = null
    let cookieString = request.headers.get('Cookie')
    let anonymousId = null;
    let expires = null;
    let cookieString = request.headers.get("Cookie");
    if (cookieString) {
    let cookies = cookieString.split(';')
    let cookies = cookieString.split(";");
    cookies.forEach(cookie => {
    let cookieName = cookie.split('=')[0].trim()
    let cookieName = cookie.split("=")[0].trim();
    if (cookieName === name) {
    anonymousId = cookie.split('=')[1]
    anonymousId = cookie.split("=")[1];
    }
    if (cookieName === `${name}_set`) {
    expires = new Date(decodeURIComponent(cookie.split('=')[1]))
    expires = new Date(decodeURIComponent(cookie.split("=")[1]));
    }
    })
    });
    }
    return { anonymousId, expires }
    return { anonymousId, expires };
    }

  14. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 2 additions and 3 deletions.
    5 changes: 2 additions & 3 deletions worker.js
    Original 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 (isSDKRequest(request)) {
    if (isScriptRequest(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)
    @@ -96,7 +95,7 @@ async function handle(event) {
    return await fetch(request)
    }

    function isSDKRequest(request) {
    function isScriptRequest(request) {
    const url = new URL(request.url)
    if (url.pathname.startsWith(`/$SCRIPT_PATH_PREFIX}`) && request.method === 'GET') return true
    return false
  15. @sperand-io sperand-io revised this gist Jan 3, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion worker.js
    Original 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 SEGMENT_API_HOST = 'api.segment.io/v1'

    const { request } = event
    const url = new URL(request.url)
  16. @sperand-io sperand-io renamed this gist Jan 3, 2020. 1 changed file with 24 additions and 20 deletions.
    44 changes: 24 additions & 20 deletions gistfile1.txt → worker.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,3 @@
    addEventListener('fetch', event => {
    event.respondWith(handle(event))
    })

    /**
    * 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}/ints/list/${writeKey}`) or fetch(`${location.origin}/ints/list/}`)
    * (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 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 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

    // 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)) {
    @@ -57,7 +55,8 @@ async function handle(event) {
    if (cached) {
    response = cached
    } else {
    const originalResponse = await fetch(new Request(`https://cdn.segment.com/analytics.js/v1/${writeKey}/analytics.min.js`, request))
    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', `${encodeURIComponent(COOKIE_NAME)}=${uuid()}; Expires=${oneYearFromNow.toUTCString()}; SameSite=Strict; Secure; HttpOnly`)

    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
    if (!writeKey) throw new Error()
    return await fetch(new Request(`https://cdn.segment.com/v1/projects/${writeKey}/integrations`, new Request(request, { body })))
    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))
  17. @sperand-io sperand-io revised this gist Jan 3, 2020. No changes.
  18. @sperand-io sperand-io created this gist Jan 3, 2020.
    161 changes: 161 additions & 0 deletions gistfile1.txt
    Original 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 }
    }