Skip to content

Commit

Permalink
Move (google#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
cramforce authored Oct 29, 2022
1 parent 93ae1d5 commit 9bf513f
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 163 deletions.
5 changes: 3 additions & 2 deletions .eleventyignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.DS_Store
.github/
.vercel/
.netlify/
_site/
\_site/
node_modules/
package-lock.json
README.md
_11ty/
\_11ty/
third_party
functions
test
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ npm run build
#### Miscellaneous

- Immutable URLs for JS.
- Sets immutable caching headers for images, fonts, and JS (CSS is inlined).
- Minifies HTML and optimizes it for compression. Uses [html-minifier](https://www.npmjs.com/package/html-minifier) with aggressive options.
- Sets immutable caching headers for images, fonts, and JS (CSS is inlined). Automatically configured when deploying on [Vercel](https://vercel.com/)
- Uses [html-minifier](https://www.npmjs.com/package/html-minifier) with aggressive options.
- Uses [rollup](https://rollupjs.org/) to bundle JS and minifies it with [terser](https://terser.org/).
- Prefetches same-origin navigations when a navigation is likely.
- If an AMP files is present, [optimizes it](https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/).
Expand All @@ -109,7 +109,7 @@ npm run build

#### Analytics

- Supports locally serving Google Analytics's JS and proxying it's hit requests to a serverless function proxy (other proxies could be easily added).
- Supports locally serving Google Analytics's JS and proxying it's hit requests to a Vercel Edge Function (other proxies could be easily added).
- Supports sending [Core Web Vitals](https://web.dev/vitals/) metrics to Google Analytics as [events](https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics).
- Support for noscript hit requests.
- Avoids blocking onload on analytics requests.
Expand Down
2 changes: 1 addition & 1 deletion _includes/layouts/base.njk
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<dialog id="message"></dialog>
{% if googleanalytics %}
<noscript>
<img src="/.netlify/functions/ga?v=1&_v=j83&t=pageview&dr=https%3A%2F%2Fno-script.com&_s=1&dh={{ metadata.domain | encodeURIComponent }}&dp={{ page.url | encodeURIComponent }}&ul=en-us&de=UTF-8&dt={{title|encodeURIComponent}}&tid={{googleanalytics}}" width="1" height="1"
<img src="/api/ga?v=1&_v=j83&t=pageview&dr=https%3A%2F%2Fno-script.com&_s=1&dh={{ metadata.domain | encodeURIComponent }}&dp={{ page.url | encodeURIComponent }}&ul=en-us&de=UTF-8&dt={{title|encodeURIComponent}}&tid={{googleanalytics}}" width="1" height="1"
style="display:none" alt="">
</noscript>
{% endif %}
Expand Down
150 changes: 150 additions & 0 deletions api/ga.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
const GA_ENDPOINT = `https://www.google-analytics.com/collect`;

// Domains to allowlist. Replace with your own!
const originallowlist = [];
// Update me.
allowlistDomain("eleventy-high-performance-blog-sample.industrialempathy.com/");

let hot = false;
let age = Date.now();

export const config = {
runtime: "experimental-edge",
};

export default async function (req, event) {
const url = new URL(req.url);
if (req.method === "GET" && !url.search) {
return new Response("OK", { status: 200 });
}

const origin = req.headers.get("origin") || "";
console.log(`Received ${req.method} request from, origin: ${origin}`);

const isOriginallowlisted =
originallowlist.indexOf(origin) >= 0 ||
origin.endsWith("-cramforce.vercel.app") ||
origin.endsWith("-team-malte.vercel.app");
if (!isOriginallowlisted) {
console.info("Bad origin", origin);
return new Response("Not found", { status: 404 });
}

let cacheControl = "no-store";
if (url.searchParams.get("ec") == "noscript") {
cacheControl = "max-age: 30";
}
const headers = {
"Access-Control-Allow-Origin": isOriginallowlisted
? origin
: originallowlist[0],
"Cache-Control": cacheControl,
"x-age": `${hot}; ${Date.now() - age}`,
};
hot = true;

event.waitUntil(proxyToGoogleAnalytics(req, url, await req.text()));
return new Response("D", { status: 200, headers });
}

function allowlistDomain(domain, addWww = true) {
const prefixes = ["https://", "http://"];
if (addWww) {
prefixes.push("https://www.");
prefixes.push("http://www.");
}
prefixes.forEach((prefix) => originallowlist.push(prefix + domain));
}

async function cid(ip, otherStuff) {
if (ip) {
const encoder = new TextEncoder();
const data = encoder.encode(
"sha256",
ip + otherStuff + "this is open source"
);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}
return Math.random() * 1000; // They use a decimal looking format. It really doesn't matter.
}

async function proxyToGoogleAnalytics(req, url, body) {
// get GA params whether GET or POST request
const params =
req.method.toUpperCase() === "GET"
? url.searchParams
: new URLSearchParams(body);
const headers = req.headers;

// attach other GA params, required for IP address since client doesn't have access to it. UA and CID can be sent from client
params.set(
"uip",
headers.get("x-forwarded-for") || headers.get("x-bb-ip") || ""
); // ip override. Look into headers for clients IP address, as opposed to IP address of host running lambda function
params.set("ua", params.get("ua") || headers.get("user-agent") || ""); // user agent override
params.set(
"cid",
params.get("cid") || (await cid(params.get("uip", params.get("ua"))))
);

const qs = params.toString();
console.info("proxying params:", qs);

const reqOptions = {
method: "POST",
headers: {
"Content-Type": "image/gif",
},
body: qs,
};
let result;
try {
result = await fetch(GA_ENDPOINT, reqOptions);
} catch (e) {
console.error("googleanalytics error!", e);
return;
}
if (result.status == 200) {
console.debug("googleanalytics request successful");
return;
}
console.error(
"googleanalytics status code",
result.status,
result.statusText
);
}

/*
Docs on GA endpoint and example params
https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide
v: 1
_v: j67
a: 751874410
t: pageview
_s: 1
dl: https://nfeld.com/contact.html
dr: https://google.com
ul: en-us
de: UTF-8
dt: Nikolay Feldman - Software Engineer
sd: 24-bit
sr: 1440x900
vp: 945x777
je: 0
_u: blabla~
jid:
gjid:
cid: 1837873423.1522911810
tid: UA-116530991-1
_gid: 1828045325.1524815793
gtm: u4d
z: 1379041260
*/
2 changes: 1 addition & 1 deletion feed/feed.njk
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ eleventyExcludeFromCollections: true
{% if googleanalytics %}
{% set titleUrlEncoded = post.data.title|encodeURIComponent %}
{% set urlUrlEncoded = post.url | encodeURIComponent %}
{{'<img src="' + metadata.url + '/.netlify/functions/ga?v=1&_v=j83&t=pageview&dr=https%3A%2F%2Frss-feed-reader.com&_s=1&dh=' + metadata.domain + '&dp=' + urlUrlEncoded + '&ul=en-us&de=UTF-8&dt=' + titleUrlEncoded + '&tid=' + googleanalytics + '" width="1" height="1" style="display:none" alt="">'}}
{{'<img src="' + metadata.url + '/api/ga?v=1&_v=j83&t=pageview&dr=https%3A%2F%2Frss-feed-reader.com&_s=1&dh=' + metadata.domain + '&dp=' + urlUrlEncoded + '&ul=en-us&de=UTF-8&dt=' + titleUrlEncoded + '&tid=' + googleanalytics + '" width="1" height="1" style="display:none" alt="">'}}
{% endif %}
</content>
</entry>
Expand Down
140 changes: 0 additions & 140 deletions functions/ga.js

This file was deleted.

2 changes: 1 addition & 1 deletion js/cached.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions netlify.toml

This file was deleted.

2 changes: 1 addition & 1 deletion test/test-generic-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe("check build output for a generic post", () => {
expect(noscript.length).to.be.greaterThan(0);
let count = 0;
for (let n of noscript) {
if (n.textContent.includes("/.netlify/functions/ga")) {
if (n.textContent.includes("/api/ga")) {
count++;
expect(n.textContent).to.contain(GA_ID);
}
Expand Down
Loading

0 comments on commit 9bf513f

Please sign in to comment.