<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>lynn.zone (blog)</title><description>Lynn&apos;s blog.</description><link>https://lynn.zone</link><item><title>You Probably Don&apos;t Need a VPN</title><link>https://lynn.zone/blog/you-probably-dont-need-a-vpn</link><guid isPermaLink="true">https://lynn.zone/blog/you-probably-dont-need-a-vpn</guid><pubDate>Sun, 13 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Given the nature of this blog, and where I usually share my content, chances are most of the people reading this probably already know everything I’m about to say here. That being said, I really wanted to write about this because I think it’s a big problem, and I think those of us who see what’s going on have a duty to call it out. If this post ends up being helpful to even one person, I’ll consider the effort well worth it.&lt;/p&gt;
&lt;p&gt;Also, a quick clarification: This is mainly going to be about &lt;em&gt;consumer&lt;/em&gt; VPN products, not business VPNs, which have very different use cases and aren’t really affected by issues like predatory marketing.&lt;/p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;
&lt;p&gt;I’ll get right to it — something that’s become a &lt;em&gt;huge&lt;/em&gt; issue in recent years is predatory VPN marketing, largely aimed at non-technical audiences, and misleading to the point where I’m pretty comfortable with just calling it a scam.&lt;/p&gt;
&lt;p&gt;You’ve almost certainly seen examples of what I’m talking about here — ads for VPN products, &lt;em&gt;especially&lt;/em&gt; in the form of stuff like YouTube video sponsorships, trying to convince you that your online privacy and security is in grave danger if you’re not using their product. Oh no!&lt;/p&gt;
&lt;p&gt;Before we continue, here’s a &lt;em&gt;really&lt;/em&gt; quick explainer of what a VPN does: It lets you tunnel your connection through one of their servers (usually, you can pick where this server is), with the tunnel between you and their server being encrypted. That’s it.&lt;/p&gt;
&lt;p&gt;Now, keeping that in mind, let’s take a quick look at the claims some of the biggest VPN vendors make on their websites:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nordvpn.com/&quot;&gt;NordVPN&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A good VPN service provides you a secure, encrypted tunnel for online traffic to flow. Nobody can see through the tunnel, get their hands on your online data, or find your real IP address and location. NordVPN is the best VPN if you’re looking for peace of mind when on public Wi-Fi. Securely access personal information or work files, encrypt your internet connection, and keep your browsing history and online identity private.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.expressvpn.com/&quot;&gt;ExpressVPN&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Going online doesn’t have to mean being exposed. Whether you’re shopping from your desk or just connecting at a cafe, keep your personal information more private and secure.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.privateinternetaccess.com/&quot;&gt;Private Internet Access&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep your data encrypted for extra security. Don’t let anyone get their hands on unencrypted emails, pictures, banking details, or anything else best kept private.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://protonvpn.com/&quot;&gt;Proton VPN&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our secure VPN sends your internet traffic through an encrypted VPN tunnel, so your passwords and confidential data stay safe, even over public or untrusted Internet connections.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Okay, so… none of these are &lt;em&gt;technically&lt;/em&gt; saying anything that’s not true. However, if we read between the lines here even a tiny bit, there’s several &lt;em&gt;wild&lt;/em&gt; claims being made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your online activity isn’t usually encrypted.&lt;/li&gt;
&lt;li&gt;People can “get their hands on your online data” if you’re not using a VPN.&lt;/li&gt;
&lt;li&gt;People can “get their hands on [your] unencrypted emails, pictures, [and] banking details” if you’re not using a VPN.&lt;/li&gt;
&lt;li&gt;Using public Wi-Fi without a VPN is unsafe.&lt;/li&gt;
&lt;li&gt;Shopping online without a VPN is unsafe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these are patently, demonstrably untrue, and they know that, which is why they’re being careful not to &lt;em&gt;explicitly&lt;/em&gt; make any of those claims.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Why&lt;/em&gt; are these claims untrue? The answer is pretty simple — in 2022, &lt;strong&gt;basically everything you do online is already encrypted&lt;/strong&gt;, thanks to the near-ubiquitous presence of &lt;a href=&quot;https://www.cloudflare.com/learning/ssl/what-is-https/&quot;&gt;HTTPS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Anytime you visit a website where the little padlock shows up in your browser, and the URL begins with &lt;code&gt;https://&lt;/code&gt; (which, again, is basically every website — certainly every website you’re going to be doing anything remotely sensitive on), or use an app that does the same thing under the hood, your connection to that site is already encrypted with, to borrow a phrase, “military-grade encryption”. It’s effectively impossible for anyone to get past this, at least not without compromising the device you’re using in some way.&lt;/p&gt;
&lt;p&gt;Now, HTTPS doesn’t quite encrypt &lt;em&gt;all&lt;/em&gt; of your traffic in the way a VPN does. Even if you’re using HTTPS, someone with unrestricted access to your traffic can usually still use it to see a few things, mainly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The IP addresses you’re connecting to.&lt;/li&gt;
&lt;li&gt;The names of the websites you’re connecting to* (&lt;strong&gt;not&lt;/strong&gt; full URLs).&lt;/li&gt;
&lt;li&gt;Any other unencrypted data you’re sending over the network, even though this &lt;em&gt;should&lt;/em&gt; be quite rare in 2022, and largely limited to stuff like games that don’t bother to encrypt their network packets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s legitimately the case that the extra layer of encryption you get with a VPN makes it so that nobody between you and the VPN server can see these anymore. That being said, it’s still clear that the reality is &lt;em&gt;much&lt;/em&gt; less scary than the wild claims VPN vendors make about what can happen if you’re not using their product, which is inconvenient for them, so they just lie instead.&lt;/p&gt;
&lt;p&gt;* There are ongoing efforts to close the remaining gaps in encryption on the web that make this possible, such as &lt;a href=&quot;https://en.wikipedia.org/wiki/DNS_over_HTTPS&quot;&gt;DNS over HTTPS&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/DNS_over_TLS&quot;&gt;DNS over TLS&lt;/a&gt;, and &lt;a href=&quot;https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni&quot;&gt;TLS Encrypted Client Hello&lt;/a&gt;, but industry-wide changes like this take &lt;em&gt;years&lt;/em&gt; to happen, so as of 2022, these haven’t been widely deployed yet.&lt;/p&gt;
&lt;h2 id=&quot;what-can-we-do-here&quot;&gt;What can we do here?&lt;/h2&gt;
&lt;p&gt;Honestly, I’m not sure what we can really do about this. The companies that do this are quite careful to not make these claims in a way that’d make it trivial to bag them for false advertising, and that’s without even getting into the fact that many of these companies are based in jurisdictions like the British Virgin Islands (ExpressVPN) and Panama (NordVPN), where I think it’s reasonable to assume consumer protections are a little weaker than in places like Europe or North America.&lt;/p&gt;
&lt;p&gt;Perhaps the most effective thing we can do here is hold accountable the army of YouTubers and other content creators who take sponsorships from these companies and make these claims on their behalf, often even going above and beyond the companies’ own marketing, as well as blurring the distinction between what they’re being paid to say and what they genuinely believe.&lt;/p&gt;
&lt;p&gt;To be clear, I don’t think there’s anything &lt;em&gt;inherently&lt;/em&gt; wrong with content creators taking sponsorships, but they have a duty to vet these sponsorships to make sure they’re not selling snake oil to their audience, and that’s clearly not happening here. I really hope we’re slowly approaching a point where people wake up to how awful a lot of these YouTube sponsorships are (not even just the ones we’re talking about here), because it sure doesn’t feel like the situation can get much worse on that front.&lt;/p&gt;
&lt;h2 id=&quot;so-should-you-use-a-vpn&quot;&gt;So, should you use a VPN?&lt;/h2&gt;
&lt;p&gt;So, should you use a VPN? Maybe! Even knowing that they don’t do nearly as much as they claim to, there are still totally legitimate reasons you might want to use one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You live in an authoritarian state where being found to have engaged in “undesirable activity” comes with potentially grave consequences, and/or want to get around the sweeping internet censorship inevitably done by such states.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You &lt;em&gt;don’t&lt;/em&gt; live in an authoritarian state, but still have reason to be concerned about the possibility of a middleman (your ISP, employer, school, parents, etc.) having access to even very limited data on what you’re doing online, or you want to get around filters that rely on that data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You want to be able to hide your real IP, or make it look like you’re in a different location. This is quite useful for gaining access to region-locked content on e.g. streaming services, and probably the most common real use case for a VPN.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That being said, if you’re just worried about your passwords getting stolen because you logged into your bank account while on Starbucks Wi-Fi… don’t waste your money. You &lt;em&gt;do not&lt;/em&gt; need a VPN to enjoy a basic level of online security.&lt;/p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;
&lt;p&gt;I’m definitely not the first person to have concerns about this or write about it, just one of many trying to move the needle in favor of people not getting scammed out of their money by predatory marketing. If you’re interested in looking into the topic a bit more, here are some other resources you might want to have a look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=WVDQEoe6ZWY&quot;&gt;Tom Scott - This Video Is Sponsored By ███ VPN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.vice.com/en/article/xgxnwk/you-probably-dont-need-a-vpn&quot;&gt;VICE - You Probably Don’t Need a VPN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>&quot;Opting Out&quot; of Tracing Headers on Google Cloud Platform</title><link>https://lynn.zone/blog/opting-out-of-tracing-on-gcp</link><guid isPermaLink="true">https://lynn.zone/blog/opting-out-of-tracing-on-gcp</guid><pubDate>Tue, 09 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’re using &lt;a href=&quot;https://cloud.google.com/run&quot;&gt;Google Cloud Run&lt;/a&gt; (and, from what I understand, this also applies to &lt;a href=&quot;https://cloud.google.com/functions&quot;&gt;Google Cloud Functions&lt;/a&gt; and &lt;a href=&quot;https://cloud.google.com/appengine&quot;&gt;Google App Engine standard&lt;/a&gt;), Google &lt;a href=&quot;https://cloud.google.com/run/docs/trace&quot;&gt;will automatically create a trace&lt;/a&gt; in &lt;a href=&quot;https://cloud.google.com/trace&quot;&gt;Cloud Trace&lt;/a&gt; for each incoming request, and inject the trace context into the request via the &lt;a href=&quot;https://www.w3.org/TR/trace-context/&quot;&gt;standard&lt;/a&gt; &lt;code&gt;traceparent&lt;/code&gt; HTTP header.&lt;/p&gt;
&lt;p&gt;This is a cool feature… if you &lt;em&gt;want&lt;/em&gt; to use Cloud Trace. If you want to send telemetry to a different vendor, though, like &lt;a href=&quot;https://www.honeycomb.io/&quot;&gt;Honeycomb&lt;/a&gt;, this is going to be a problem, because if your app is set up to pick up the trace context from the incoming request’s headers, your telemetry will contain references to “missing” root spans that only exist in Cloud Trace.&lt;/p&gt;
&lt;p&gt;In Honeycomb, that’ll look something like this, and will prevent a lot of queries from working properly:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A screenshot of Honeycomb, showing a trace with a missing root span, and the following warning: &amp;quot;The root span of this trace is missing. There may be other spans missing, and the waterfall might render incorrectly. Expand the time range of the query and try again?&amp;quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1072&quot; height=&quot;268&quot; src=&quot;https://lynn.zone/.netlify/images?url=_astro%2Fmissing-root-span.BGAf_kpm.png&amp;amp;w=1072&amp;amp;h=268&quot;&gt;&lt;/p&gt;
&lt;p&gt;In my Node.js services, instrumented using &lt;a href=&quot;https://opentelemetry.io/&quot;&gt;OpenTelemetry&lt;/a&gt; and its included Node.js auto-instrumentations, I “fixed” this issue by using the hooks provided by the HTTP instrumentation to discard any headers Cloud Tracing might be setting before the incoming request gets processed into a span. You can do this by replacing your call to &lt;code&gt;getNodeAutoInstrumentations()&lt;/code&gt; with this code:&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;getNodeAutoInstrumentations&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;  &apos;@opentelemetry/instrumentation-http&apos;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;    startIncomingSpanHook&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: (&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;      delete&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; req&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;traceparent&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;      delete&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; req&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`x-cloud-trace-context`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;      delete&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; req&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`grpc-trace-bin`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;}),&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(If you’re setting up &lt;code&gt;@opentelemetry/instrumentation-http&lt;/code&gt; separately, you can pass the same config object to the &lt;code&gt;HttpInstrumentation&lt;/code&gt; constructor.)&lt;/p&gt;
&lt;p&gt;This is, unfortunately, clearly a hack, but also the least painful way I could find of getting around this issue. Other potential solutions I considered were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disable Cloud Trace&lt;/strong&gt; — You can’t outright disable Cloud Trace for a project without also disabling a bunch of other services that depend on it, and I’m not sure if doing that would even actually prevent this behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a reverse proxy to strip the headers&lt;/strong&gt; — On Cloud Run, you could theoretically solve this by adding some kind of reverse proxy to your container, and configuring it to strip these headers before they make it to your app. This would (in my opinion, anyway) be a massively overengineered solution, though, and also negatively impact your app’s startup time, which is especially important in a serverless context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please let me know if you’ve also run into this and figured out a better way of getting around it!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; Thanks to Andreas for the suggestion of using &lt;a href=&quot;https://github.com/openzipkin/b3-propagation&quot;&gt;B3 Propagation&lt;/a&gt; as another potential solution!&lt;/p&gt;
&lt;p&gt;B3 Propagation is a &lt;a href=&quot;https://opentelemetry.io/docs/reference/specification/context/api-propagators/#propagators-distribution&quot;&gt;natively-supported&lt;/a&gt; method for trace context propagation by OpenTelemetry, but &lt;em&gt;not&lt;/em&gt; by GCP, so configuring OpenTelemetry to use B3 Propagation rather than the W3C Trace Context spec is a convenient, robust and non-hacky way to make sure GCP won’t mess with your telemetry.&lt;/p&gt;
&lt;p&gt;The main drawback to this method is that you may not be able to do this if you, for example, need to interface with other systems that expect W3C Trace Context headers rather than B3 headers. It’s also possible (but, in fairness, probably not super likely) that GCP will introduce support for B3 Propagation in the future, which would potentially break this solution.&lt;/p&gt;
&lt;p&gt;I also personally have a bit of philosophical issue with perpetuating the status quo of there being multiple competing standards for doing the same thing, but realistically, even if you’re like me and agree that there should really be &lt;em&gt;one&lt;/em&gt; standard for this, there isn’t really much harm being done by using a feature that OpenTelemetry has already committed to supporting for the foreseeable future.&lt;/p&gt;
&lt;p&gt;On a final note, if B3 Propagation doesn’t work for you for whatever reason, everything I’ve said here theoretically holds true for any propagation standard natively supported by OpenTelemetry that &lt;em&gt;isn’t&lt;/em&gt; W3C Trace Context, which (at time of writing), in addition to B3 Propagation, also includes &lt;a href=&quot;https://w3c.github.io/baggage/&quot;&gt;W3C Baggage&lt;/a&gt; and the &lt;a href=&quot;https://www.jaegertracing.io/docs/1.38/client-libraries/#propagation-format&quot;&gt;Jaeger propagation format&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>Service-to-Service Communication on Google Cloud Run with tRPC</title><link>https://lynn.zone/blog/trpc-service-to-service-on-cloud-run</link><guid isPermaLink="true">https://lynn.zone/blog/trpc-service-to-service-on-cloud-run</guid><pubDate>Thu, 04 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://trpc.io/&quot;&gt;tRPC&lt;/a&gt; is a TypeScript library that’s usually used to enable boilerplate-free and typesafe API calls between backend and frontend code in the same app, but it works great for service-to-service communication, too.&lt;/p&gt;
&lt;p&gt;I recently needed to implement service-to-service communication with tRPC (while building &lt;a href=&quot;https://usekonbini.com/&quot;&gt;Konbini&lt;/a&gt;!) on &lt;a href=&quot;https://cloud.google.com/run&quot;&gt;Google Cloud Run&lt;/a&gt;, which makes this just a touch harder to do than usual, because unless you configure the service you’re calling to allow unauthenticated requests from anyone on the internet (which, if it’s an internal service, you probably don’t want to do), requests to it need to include authentication headers for a service account that’s allowed to make calls to that service.&lt;/p&gt;
&lt;p&gt;(You may be used to solving this problem by just putting your services on a private network that isn’t reachable from the internet, but Cloud Run doesn’t let you do that.)&lt;/p&gt;
&lt;p&gt;Thankfully, tRPC lets us set those headers, and the &lt;a href=&quot;https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest&quot;&gt;Google Auth Library&lt;/a&gt; helps us generate them. (Note that this library only supports Node.js, so if you’re running a different JS runtime on Cloud Run, you’ll need to &lt;a href=&quot;https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#applications&quot;&gt;fetch a token from the metadata server&lt;/a&gt; yourself.)&lt;/p&gt;
&lt;p&gt;Anyway, here’s the gist of what I ended up doing:&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;AppRouter&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;../../../other-service/src/trpc&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;createTRPCClient&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;@trpc/client&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;auth&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; googleAuth&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;google-auth-library&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt; createTRPCClient&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;&amp;#x3C;AppRouter&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;OTHER_SERVICE_URL&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;}/trpc`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;  headers&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;NODE_ENV&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; ===&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; `production`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; tokenClient&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; googleAuth&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.getIdTokenClient&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;        process&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;OTHER_SERVICE_URL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; tokenClient&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.getRequestHeaders&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is very similar to the code you’d normally write to &lt;a href=&quot;https://trpc.io/docs/vanilla&quot;&gt;create a tRPC client&lt;/a&gt;, but with a bit of additional code to set the headers we need on each request.&lt;/p&gt;
&lt;p&gt;If this seems suspiciously simple, it’s because the Google Auth Library automatically pulls the credentials it needs from the Cloud Run environment, so you don’t need to configure anything — it just gives you the headers.&lt;/p&gt;
&lt;p&gt;Note that your requests will still fail if the service account assigned to the client service isn’t allowed to call the other service. All Cloud Run services use the default Compute Engine service account by default, though, so it should work out of the box.&lt;/p&gt;
&lt;p&gt;Feel free to reach out if you run into problems with this approach, or if you just have other thoughts about it!&lt;/p&gt;</content:encoded></item><item><title>E2E Testing an App with Clerk Authentication in Cypress</title><link>https://lynn.zone/blog/e2e-testing-clerk-authentication-in-cypress</link><guid isPermaLink="true">https://lynn.zone/blog/e2e-testing-clerk-authentication-in-cypress</guid><pubDate>Thu, 21 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Background: &lt;a href=&quot;https://clerk.dev/&quot;&gt;Clerk&lt;/a&gt; is a hosted authentication and user management product.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I recently started writing E2E tests in Cypress for an app that uses &lt;a href=&quot;https://clerk.dev/&quot;&gt;Clerk&lt;/a&gt; for authentication, and there wasn’t anything out there to guide me, so here’s what I ended up with after fiddling with it for a bit.&lt;/p&gt;
&lt;p&gt;(Note: In my case, this is a Next.js app using Clerk’s Next.js SDK, but my understanding is that this code will work everywhere, because their client SDKs all ultimately use ClerkJS under the hood.)&lt;/p&gt;
&lt;p&gt;I wrote a &lt;a href=&quot;https://docs.cypress.io/api/cypress-api/custom-commands&quot;&gt;custom Cypress command&lt;/a&gt; that waits for Clerk to load, signs the user out if they aren’t signed out already, and then signs in with test credentials (see &lt;a href=&quot;https://docs.cypress.io/guides/guides/environment-variables&quot;&gt;here&lt;/a&gt; for how you can set these so that they’re accessible via &lt;code&gt;Cypress.env()&lt;/code&gt;).&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Cypress&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Commands&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.add&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`initializeAuth`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, () &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  cy&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.log&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`Initializing auth state.`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  cy&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.visit&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`/`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  cy&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.window&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;    .should&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;      expect&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;to&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;have&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.property&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`Clerk`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;undefined&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;      expect&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Clerk&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.isReady&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;()).&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;to&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.eq&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;    .then&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;      await&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; window&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Clerk&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.signOut&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;      await&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; window&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Clerk&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;client&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;signIn&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.create&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;        identifier: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Cypress&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.env&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`TEST_USER_IDENTIFIER`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;        password: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Cypress&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.env&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`TEST_USER_PASSWORD`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’re using TypeScript to write your tests (which I recommend!), you’ll also want to add this command to your &lt;a href=&quot;https://docs.cypress.io/guides/tooling/typescript-support#Types-for-custom-commands&quot;&gt;custom command types&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;declare&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; global&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  namespace&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; Cypress {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;    interface&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; Chainable {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;      /**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;       * Initialize auth to a state where you&apos;re&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;       * logged in as the test user.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;       *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;       * &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;@example&lt;/span&gt;&lt;span style=&quot;color:#969896&quot;&gt; cy.initializeAuth()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;       */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;      initializeAuth&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; Chainable&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F0C674&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ultimately, you’ll probably want to use this command in a &lt;code&gt;before&lt;/code&gt; or &lt;code&gt;beforeEach&lt;/code&gt; hook to reset the auth state before every test, like so:&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;describe&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`Test Page`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, () &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;  beforeEach&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    cy&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.resetDatabase&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#969896&quot;&gt;// another custom command&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    cy&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.initializeAuth&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // ... tests go here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Happy testing! Please let me know if you run into problems with this approach.&lt;/p&gt;</content:encoded></item><item><title>Simple, Privacy-Friendly, and Free Analytics Using Serverless Functions</title><link>https://lynn.zone/blog/serverless-analytics</link><guid isPermaLink="true">https://lynn.zone/blog/serverless-analytics</guid><pubDate>Thu, 15 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Whether it’s for work or play, most of us usually want &lt;em&gt;some&lt;/em&gt; analytics on our websites, but the available options typically come with one or more pretty major downsides:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Massive JavaScript payload&lt;/strong&gt; — Most of the big analytics products have this problem. &lt;code&gt;gtag.js&lt;/code&gt; is ~35kB minzipped, ~90kB uncompressed (at time of writing); some others are even worse. Yes, these days this is loaded asynchronously and doesn’t directly hold up your page while it gets loaded and parsed, but that’s still an absolutely indefensible amount of overhead just to get some super basic data on how your site is being used.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Privacy implications&lt;/strong&gt; — Again, this is one you’re almost certainly going to hit if you’re looking at a free, hosted analytics product, like Google Analytics or Yandex.Metrica. Third-party cookies may thankfully be going the way of the dodo, but tools like Google Analytics — if you use the tracking code they give you — will still insist on giving your users at least first-party cookies they can use to identify them across multiple requests (and visits), regardless of whether you want that feature or not.&lt;/p&gt;
&lt;p&gt;From a practical perspective, this means you now need to ask your users to consent to these tracking cookies, and from an ethical perspective, you may just want to avoid tracking your users like this altogether, especially if it means doing it through someone like Google.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inaccuracy due to people blocking analytics&lt;/strong&gt; — Whether it’s through privacy-enhancing browser features, browser extensions like &lt;a href=&quot;https://github.com/gorhill/uBlock&quot;&gt;uBlock Origin&lt;/a&gt; or even something like &lt;a href=&quot;https://github.com/pi-hole/pi-hole&quot;&gt;Pi-hole&lt;/a&gt;, lots of people these days are running something that blocks obvious analytics requests. Depending on how privacy-conscious your audience is, as many as &lt;a href=&quot;https://blog.wesleyac.com/posts/google-analytics&quot;&gt;~45%&lt;/a&gt; of your visitors could be blocking analytics entirely.&lt;/p&gt;
&lt;p&gt;Serverside analytics naturally don’t suffer from this problem, and some privacy-focused analytics products like &lt;a href=&quot;https://usefathom.com/support/custom-domains&quot;&gt;Fathom Analytics&lt;/a&gt; and &lt;a href=&quot;https://plausible.io/docs/custom-domain&quot;&gt;Plausible Analytics&lt;/a&gt; give you ways to use your own domain for analytics to avoid being caught by this, but if you’re using a typical big-name analytics product (unless you’re being particularly clever about it, which we’ll get to), this is a huge drawback you’re kind of just stuck with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; — If you’re looking to add analytics to something like a personal site, this is probably your biggest problem. Modern, privacy-focused analytics products like &lt;a href=&quot;https://usefathom.com/&quot;&gt;Fathom Analytics&lt;/a&gt;, &lt;a href=&quot;https://plausible.io/&quot;&gt;Plausible Analytics&lt;/a&gt; and &lt;a href=&quot;https://www.netlify.com/products/analytics/&quot;&gt;Netlify Analytics&lt;/a&gt; give you ways to avoid many or even all of the other problems on this list, but they’re not free — and there’s nothing wrong with that, but I can’t justify paying a monthly subscription just to see how many people read my blog posts, and you probably can’t either.&lt;/p&gt;
&lt;p&gt;One way to get around this could be self-hosting an open-source analytics product like &lt;a href=&quot;https://matomo.org/&quot;&gt;Matomo Analytics&lt;/a&gt;, but unless you already have somewhere you can put that without incurring any extra costs, that costs money, too.&lt;/p&gt;
&lt;p&gt;That being said, if you’re looking for an analytics solution for your business or something else that actually makes money, I think you should strongly consider just paying for one of these products and being done with it.&lt;/p&gt;
&lt;h2 id=&quot;enter-serverless&quot;&gt;Enter Serverless&lt;/h2&gt;
&lt;p&gt;Okay, so what can we do about this? Well, we can be smart about the way we use analytics, and take control of how our analytics are collected while still leaving the bulk of the work to a third-party analytics product (if we want to).&lt;/p&gt;
&lt;p&gt;How do we do that? With a serverless function.&lt;/p&gt;
&lt;p&gt;More specifically, here’s what this approach entails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Write a serverless function that takes an analytics event — whatever that means to you — and pipes it wherever you want. This gives you complete control over what and how much data you send, what you do with it before sending it to a third party, and where it ends up.&lt;/p&gt;
&lt;p&gt;The most immediately useful thing you could do here is pipe the event to a traditional analytics product like Google Analytics (giving you the benefits of a proper analytics product without many of the downsides we talked about), and I imagine this is what most people will want to do, but the place you store your events can be literally anything — a log file, a database you can later build your own analytics tools on top of, hell, it could even be a spreadsheet.&lt;/p&gt;
&lt;p&gt;You could also do stuff like pipe events to your own database while still sending them to Google Analytics, so you still own all of your data if you ever want to do something with it that Google Analytics doesn’t let you do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Write a tiny bit of clientside code (which, again, you now have complete control over) to send events to your analytics function.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If we go over those downsides we mentioned at the start of this post again, we can see that this approach allows us to pretty much completely sidestep them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Massive JavaScript payload&lt;/strong&gt; — There is none, our clientside code can be as simple as a single &lt;code&gt;fetch()&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy implications&lt;/strong&gt; — More-or-less none, we don’t have to track anything we don’t want to. Google will still have your data if that’s where you choose to pipe it, but there’s not a lot they can do with it if it’s all anonymized and not tied to a specific user in any way (though you can still do that if you want to, of course).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inaccuracy due to people blocking analytics&lt;/strong&gt; — Our analytics requests won’t be blocked because they’re not tied to a known analytics product. As long as we don’t do something like use the word &lt;code&gt;analytics&lt;/code&gt; in the name of our endpoint, they’ll just look like any other request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt; — On the analytics side this allows us to use free products like Google Analytics without many of the downsides they usually come with, and as for our serverless function, chances are you won’t ever have to pay a dime to run it with how generous pretty much every platform’s free tier is. For instance, AWS Lambda gives you &lt;a href=&quot;https://aws.amazon.com/lambda/pricing/&quot;&gt;one million free requests&lt;/a&gt; every month (at time of writing).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-this-looks-like-in-practice&quot;&gt;What This Looks Like in Practice&lt;/h2&gt;
&lt;p&gt;I have to stress that this post is much more about the general concept than it is about a specific implementation — you don’t &lt;em&gt;have&lt;/em&gt; to do anything here the same way I did it. That being said, I’m including an example of how I implemented this pattern for my own website (&lt;a href=&quot;https://veselin.dev&quot;&gt;veselin.dev&lt;/a&gt;) because I think it’ll be helpful, and because I think my usecase is common enough that many people should be able to take what I did here and carry it over to their own website(s) with few or no changes.&lt;/p&gt;
&lt;p&gt;Here’s my analytics function. It’s a Netlify Functions function that mostly just proxies events to Google Analytics using the &lt;a href=&quot;https://developers.google.com/analytics/devguides/collection/protocol/v1&quot;&gt;Measurement Protocol&lt;/a&gt; (note: I’m using Universal Analytics here because the Measurement Protocol for GA4 is &lt;a href=&quot;https://developers.google.com/analytics/devguides/collection/protocol/ga4&quot;&gt;still in alpha&lt;/a&gt;). It anonymizes IP addresses before sending them to Google (though it also enables Google Analytics’ &lt;a href=&quot;https://support.google.com/analytics/answer/2763052?hl=en&quot;&gt;IP anonymization&lt;/a&gt; feature that does the same thing, because why not), and sidesteps Google’s requirement of a user identifier (a &lt;code&gt;cid&lt;/code&gt; or &lt;code&gt;uid&lt;/code&gt;) by just generating a new one for every event.&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;Handler&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;APIGatewayEvent&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;aws-lambda&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; axios&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;axios&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;URLSearchParams&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; uuid&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;uuid&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; anonymizeIP&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;ip-anonymize&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;interface&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; RequestBody {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  type&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;pageview&quot;&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; &quot;event&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  params&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F0C674&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;interface&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; Response {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  statusCode&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F0C674&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt; handler&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; Handler&amp;#x3C;APIGatewayEvent, Response&gt; &lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; body&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; RequestBody &lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.parse&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt; URLSearchParams&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;v&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // enable IP anonymization, even though we&apos;re doing it here anyway&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;aip&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // GA makes us send a cid parameter, so we send a new UUID every time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // because we don&apos;t actually want to track users across requests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;cid&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;uuid&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.v4&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // Override user agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;ua&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;user-agent&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // Override user IP (but anonymize it first)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;uip&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;anonymizeIP&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;client-ip&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;]));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // Set event data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;tid&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;GOOGLE_ANALYTICS_TRACKING_ID&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;t&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; paramName&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; body&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;params&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.append&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;paramName&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;params&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;paramName&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // Send event to Google Analytics&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; axios&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.post&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;    &quot;https://www.google-analytics.com/collect&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    analyticsRequestBody&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.toString&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { statusCode: &lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;handler&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; };&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the time being I’m happy with this solution, but you could expand on this by implementing something like Plausible’s daily identifier method (described &lt;a href=&quot;https://plausible.io/data-policy#how-we-count-unique-users-without-cookies&quot;&gt;here&lt;/a&gt;) to anonymously identify unique users, and sending that to Google as your &lt;code&gt;uid&lt;/code&gt;. (&lt;strong&gt;Update:&lt;/strong&gt; I’ve since published &lt;a href=&quot;https://github.com/omegavesko/anonymous-user-id&quot;&gt;anonymous-user-id&lt;/a&gt;, an npm package that helps you do this!)&lt;/p&gt;
&lt;p&gt;My clientside code is basically a single &lt;code&gt;fetch()&lt;/code&gt; call that sends the same data Google Analytics usually sends for a &lt;code&gt;pageview&lt;/code&gt; event. You’ll want to put this code somewhere it’ll be called both on the initial pageload and whenever the user navigates to a different page — in my case, that’s Gatsby’s &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#onRouteUpdate&quot;&gt;onRouteUpdate&lt;/a&gt; hook.&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;NODE_ENV&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; !==&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; `production`&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt; sendPageView&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; pagePath&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; location&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;    ?&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; location&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;pathname&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; location&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;search&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; location&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;hash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;    :&lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt; undefined&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;  fetch&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;/.netlify/functions/event&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    method: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    headers: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;      &quot;Content-Type&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    body: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;.stringify&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      type: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;pageview&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      params: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;        dh: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;hostname&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;        dp: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;pagePath&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;        dt: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;        dr: &lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;referrer&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;// wrap inside a rAF to make sure react-helmet is done with its changes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;// (https://github.com/gatsbyjs/gatsby/issues/11592)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;`requestAnimationFrame`&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt; window&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;  requestAnimationFrame&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;    requestAnimationFrame&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;sendPageView&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;  // simulate 2 rAF calls&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;  setTimeout&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;sendPageView&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#DE935F&quot;&gt;32&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’m not sending any custom events on my site right now, but those would be handled similarly. If you use analytics heavily on your site, you’ll probably want to extract this code into a utility function rather than duplicating it every time you want to send an event.&lt;/p&gt;
&lt;h2 id=&quot;why-serverless&quot;&gt;Why Serverless?&lt;/h2&gt;
&lt;p&gt;You wouldn’t be wrong to notice that everything we’re doing here could be implemented just fine in a traditional backend architecture. So, why am I focusing on serverless specifically? Well, there’s a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More and more sites (like the one I talk about in my example!) are being built on &lt;a href=&quot;https://jamstack.org/&quot;&gt;Jamstack&lt;/a&gt; or serverless architectures without a traditional backend. Using a serverless function for this works equally well regardless of what your existing stack is.&lt;/li&gt;
&lt;li&gt;Even if you have an existing backend you could integrate this functionality into, using a serverless function has the benefit of ensuring that you don’t have to worry about the performance impact of handling all these extra requests that would typically be handled directly by a third-party service.&lt;/li&gt;
&lt;li&gt;While this isn’t a usecase I talk about a lot in this post, a serverless function not tied to an existing backend is easier to use to collect events from a bunch of different places at once.&lt;/li&gt;
&lt;li&gt;Circling back to the Jamstack point again, serverless functions are inherently infinitely scalable. If you have a site deployed to something like a CDN — where your site will never get hugged to death — the last thing you want to have to worry about is whether your analytics can keep up with your traffic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That being said, of course you could apply all of these ideas to a traditional backend, too. I just focus on serverless as a solution in this post because I think it’s a great fit for this usecase.&lt;/p&gt;
&lt;h2 id=&quot;the-ethics-of-circumventing-blockers&quot;&gt;The Ethics of Circumventing Blockers&lt;/h2&gt;
&lt;p&gt;To address a bit of an elephant in the room, yes, a significant part of why this pattern is useful is that it allows you to do analytics for users running browser extensions or other tools that would typically block them.&lt;/p&gt;
&lt;p&gt;My opinion on this overlaps significantly with &lt;a href=&quot;https://usefathom.com/blog/bypass-adblockers&quot;&gt;Fathom’s answer to this question&lt;/a&gt; in their post announcing their custom domain feature (which is explicitly designed to do this) — I don’t believe this is unethical because the reason people block analytics is that they’re traditionally extremely invasive, and if you’re doing what I’ve described here, you’re doing everything possible to protect your users’ privacy.&lt;/p&gt;
&lt;p&gt;Or, to put it a different way — you’re not collecting any more data than serverside analytics would be, and nobody thinks serverside analytics are unethical because they can’t be blocked by a browser extension, right?&lt;/p&gt;
&lt;p&gt;That being said, it’s possible to use the techniques I’ve talked about here in an unethical way, for example by proxying requests to Google Analytics through your own domain, but not doing anything to mitigate the privacy concerns of using Google Analytics. All I can do there is ask that you please don’t do that.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That’s pretty much it! I’m very curious to see how many other people have stumbled their way into similar solutions before (I know I’ve seen stuff like people &lt;code&gt;proxy_pass&lt;/code&gt;ing requests to Google Analytics with nginx to make them less obvious), and I’m also curious to see if anyone comes up with more creative ways of building on top of this concept than just piping events to Google Analytics.&lt;/p&gt;</content:encoded></item><item><title>PHP Debugging With Xdebug 3 Inside a Docker Container</title><link>https://lynn.zone/blog/xdebug-3-in-docker</link><guid isPermaLink="true">https://lynn.zone/blog/xdebug-3-in-docker</guid><pubDate>Tue, 13 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently spent a good few hours getting Xdebug to work with my development setup (which is &lt;a href=&quot;https://www.jetbrains.com/phpstorm/&quot;&gt;PhpStorm&lt;/a&gt; running &lt;a href=&quot;https://susi.dev/dev-env-2020&quot;&gt;inside WSL 2&lt;/a&gt; on Windows 10, and PHP/Xdebug running inside a Docker container, inside WSL 2, with &lt;a href=&quot;https://docs.docker.com/docker-for-windows/install/&quot;&gt;Docker Desktop&lt;/a&gt;), so here I am writing up the surprisingly simple solution I ended up with — partially for my own future reference, but also to help out anyone who finds themselves in a similar situation.&lt;/p&gt;
&lt;p&gt;(Don’t get discouraged by my ultra-convoluted setup — this configuration &lt;em&gt;should&lt;/em&gt; actually theoretically work for pretty much any environment, for reasons I’ll get into a little later on.)&lt;/p&gt;
&lt;h2 id=&quot;preamble&quot;&gt;Preamble&lt;/h2&gt;
&lt;p&gt;Xdebug can be tricky to configure, because it works in reverse from the way you’re probably used to interacting with your PHP application — instead of sending requests &lt;em&gt;to&lt;/em&gt; your PHP code, Xdebug needs to know how to send requests &lt;em&gt;from&lt;/em&gt; where your code is running &lt;em&gt;to&lt;/em&gt; your client application (i.e. probably your editor or IDE), in order to establish a debugging connection.&lt;/p&gt;
&lt;p&gt;This is fine if your setup is relatively simple (i.e. Xdebug can just go to &lt;code&gt;localhost:9003&lt;/code&gt; and hit your client), but if your setup is a little more complicated than that — i.e. if PHP/Xdebug is running on a different physical machine, in a VM, in a container, or similar — it won’t work out of the box.&lt;/p&gt;
&lt;p&gt;Xdebug 3 makes this whole exercise a little easier for us by generally overhauling the way Xdebug is configured to make it simpler, but if your setup is a little convoluted (like mine), it can still be finicky to get right, hence this post.&lt;/p&gt;
&lt;h2 id=&quot;the-meat-and-potatoes&quot;&gt;The Meat and Potatoes&lt;/h2&gt;
&lt;p&gt;Okay, so here’s what I ended up with.&lt;/p&gt;
&lt;p&gt;This is the bit you need to put somewhere in your PHP configuration (i.e. your &lt;code&gt;php.ini&lt;/code&gt;, or wherever you usually configure your PHP extensions):&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;[Xdebug]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;xdebug.mode&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;=debug&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;xdebug.start_with_request&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;=yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;xdebug.discover_client_host&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;=true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B294BB&quot;&gt;xdebug.client_host&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;=host.docker.internal&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s go over this line-by-line:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xdebug.mode=debug&lt;/code&gt; enables &lt;a href=&quot;https://xdebug.org/docs/step_debug&quot;&gt;step debugging&lt;/a&gt; (which is probably what you want to use Xdebug for.)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xdebug.start_with_request=yes&lt;/code&gt; tells Xdebug that we want to activate step debugging at the start of every request, for simplicity’s sake. I recommend setting this to &lt;code&gt;yes&lt;/code&gt; and forgetting about it, but if you’ve done this before and you know you prefer to activate step debugging only for specific requests, leave this line out and Xdebug will default to &lt;code&gt;trigger&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The last two lines are where it gets interesting. &lt;code&gt;xdebug.discover_client_host=true&lt;/code&gt; tells Xdebug to attempt to extract the IP of the client from the HTTP request, and &lt;code&gt;xdebug.client_host&lt;/code&gt; tells it what to try if that doesn’t work.&lt;/p&gt;
&lt;p&gt;With all of these set the way we’ve set them, here’s what happens every time you send a request to your PHP application:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xdebug activates step debugging (because our &lt;code&gt;xdebug.mode&lt;/code&gt; is &lt;code&gt;debug&lt;/code&gt;, and &lt;code&gt;xdebug.start_with_request&lt;/code&gt; is set to &lt;code&gt;yes&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Xdebug attempts to automatically connect to the host the HTTP request came from (using &lt;code&gt;$_SERVER[&apos;REMOTE_ADDR&apos;]&lt;/code&gt;, &lt;code&gt;$_SERVER[&apos;HTTP_X_FORWARDED_FOR&apos;]&lt;/code&gt;, or a custom HTTP header you’ve configured).&lt;/li&gt;
&lt;li&gt;If the above didn’t work, it falls back to trying to connect to our &lt;code&gt;xdebug.client_host&lt;/code&gt;, which is &lt;code&gt;host.docker.internal&lt;/code&gt;, a DNS name for the host helpfully set for us by Docker Desktop on Windows or macOS (though unfortunately not on Linux).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fact that Xdebug lets us set both &lt;code&gt;xdebug.discover_client_host&lt;/code&gt; and &lt;code&gt;xdebug.client_host&lt;/code&gt; as a fallback is the key to making this configuration work for just about any setup, whether you’re on Linux, macOS or Windows, and even whether you’re using Docker or not.&lt;/p&gt;
&lt;p&gt;If you’re using Docker Desktop, having &lt;code&gt;host.docker.internal&lt;/code&gt; set as a fallback host means Xdebug will always be able to find its way to the host, and on most other setups, client discovery (via &lt;code&gt;xdebug.discover_client_host&lt;/code&gt;) should just work. Theoretically, the only scenario this config should break in is if you’re using something that breaks client discovery that &lt;em&gt;isn’t&lt;/em&gt; Docker (e.g. a Vagrant virtual machine), in which case there’s probably something else you can set &lt;code&gt;xdebug.client_host&lt;/code&gt; to to fix it.&lt;/p&gt;
&lt;h2 id=&quot;installing-xdebug-in-your-docker-image&quot;&gt;Installing Xdebug in Your Docker Image&lt;/h2&gt;
&lt;p&gt;If you’re using Docker (and if you’re not, I highly recommend thinking about it!), the below is an example of what I do to install and enable Xdebug in development without carrying it over to the production image, using &lt;a href=&quot;https://docs.docker.com/develop/develop-images/multistage-build/&quot;&gt;multi-stage builds&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;docker&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;# (or your base image)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; php:8.0-fpm-alpine &lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; base&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;# whatever you do to set up your image&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;# for example:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;#   - copying your source code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;#   - installing extensions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;#   - doing a `composer install`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; base &lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; development&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; pecl install xdebug &amp;#x26;&amp;#x26; docker-php-ext-enable xdebug&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; base &lt;/span&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt; production&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;# whatever you do to prepare your image for production&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#969896&quot;&gt;# for example: removing packages you don&apos;t need in production&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That’s it! You should now have an Xdebug config that works for everything from Docker Desktop to just running PHP on the same Linux machine as your editor.&lt;/p&gt;
&lt;p&gt;If you’re using this config and you’ve found a setup it doesn’t work for, please yell at me and I’ll probably add a note to this post!&lt;/p&gt;</content:encoded></item><item><title>On Docs, DX, and Developer Happiness</title><link>https://lynn.zone/blog/on-docs-dx-and-developer-happiness</link><guid isPermaLink="true">https://lynn.zone/blog/on-docs-dx-and-developer-happiness</guid><pubDate>Sun, 01 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One thing I’ve increasingly taken notice of recently is that a lot of developers, especially in corporate environments, seem to tend to put a very low priority on the DX of their projects. Documentation is poor or non-existent; setting up a working development environment is like pulling teeth. You’ve got tickets to close and you’re sure they’ll figure it out, so who cares, right?&lt;/p&gt;
&lt;p&gt;To be clear — I totally understand why this happens, and I’m not here to judge anyone who might’ve recognized some of their own projects in that description. I do, however, strongly believe that you &lt;em&gt;should&lt;/em&gt; care, and I’m here to tell you why.&lt;/p&gt;
&lt;h2 id=&quot;a-little-work-now--a-lot-less-work-later&quot;&gt;A Little Work Now = A Lot Less Work Later&lt;/h2&gt;
&lt;p&gt;Think about how many times in your life you’re going to find yourself setting up a local dev environment for a given project. At the very least, you’re probably going to do it every time you get a new dev machine and &lt;code&gt;git clone&lt;/code&gt; all the stuff you’re working on, right? You’re also going to do it every time you help someone do the same, e.g. a new person on your team.&lt;/p&gt;
&lt;p&gt;If your project is a pain to set up, that quickly adds up to a &lt;em&gt;lot&lt;/em&gt; of unnecessary frustration and wasted time. Doing this by hand is also bound to breed subtle inconsistencies in each environment, potentially leading to bugs.&lt;/p&gt;
&lt;p&gt;On top of that, if &lt;em&gt;running&lt;/em&gt; your app involves more than a single command (e.g. you need to run a web server &lt;em&gt;and&lt;/em&gt; something like a module bundler), you have to remember to do this every single time you work on it.&lt;/p&gt;
&lt;h2 id=&quot;automate-automate-automate&quot;&gt;Automate, Automate, Automate&lt;/h2&gt;
&lt;p&gt;Okay, so we’re aware that some of our processes may be inefficient. How do we fix it?&lt;/p&gt;
&lt;p&gt;The absolute best way you can choose to deal with this issue is by codifying your app’s environment and other setup into some sort of automated tooling that can do all of this for you. Luckily, we live in the age of &lt;a href=&quot;https://www.docker.com&quot;&gt;Docker&lt;/a&gt; and &lt;a href=&quot;https://www.docker.com/resources/what-container&quot;&gt;containerization&lt;/a&gt;, which means the tools for this we have at our disposal today are immensely more powerful than they were barely a few years ago.&lt;/p&gt;
&lt;p&gt;This post isn’t really the place for a deep dive into using Docker as a development environment, but the gist of what you want to do is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a &lt;code&gt;Dockerfile&lt;/code&gt; to codify as much as you can about the &lt;em&gt;immediate&lt;/em&gt; environment your app needs to run — this should include things like your language runtime, any binaries it needs (e.g. &lt;code&gt;imagemagick&lt;/code&gt;), config files, and so on.&lt;/li&gt;
&lt;li&gt;Use a &lt;code&gt;docker-compose.yml&lt;/code&gt; to codify as much as you can about your app’s &lt;em&gt;external&lt;/em&gt; dependencies, such as cache servers, database servers, mail servers, and so on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will undoubtedly take some time, especially so if you first have to take the time to understand what this Docker thing is all about. Once everything’s said and done, however, if you did everything right, you’ll have reduced the process to set up a working instance of your app to this:&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;your-rep&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;o&lt;/span&gt;&lt;span style=&quot;color:#8ABEB7&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; .&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A2BE&quot;&gt;docker-compose&lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt; up&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On top of that, if you’ve already done this once, it’ll take you a lot less time to do it in the future — especially if your apps share a common stack. It takes me maybe an hour to set all this stuff up for a typical new project these days.&lt;/p&gt;
&lt;h2 id=&quot;learning-by-example&quot;&gt;Learning by Example&lt;/h2&gt;
&lt;p&gt;If the above seems a little too abstract, let’s look at a simple example of a &lt;code&gt;docker-compose.yml&lt;/code&gt; you could use for a WordPress project.&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;&quot;3&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;services&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  wp&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    image&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;wordpress:latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    ports&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;8080:80&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    volumes&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;./wordpress:/var/www/html:rw&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    environment&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;      WORDPRESS_DB_HOST&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;db&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;      WORDPRESS_DB_NAME&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;wordpress&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;      WORDPRESS_DB_USER&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;root&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;      WORDPRESS_DB_PASSWORD&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;password&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    depends_on&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;db&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    links&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;db&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;  db&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    image&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;mysql:latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    ports&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;3306:3306&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    command&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;        &quot;--default_authentication_plugin=mysql_native_password&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;        &quot;--character-set-server=utf8mb4&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;        &quot;--collation-server=utf8mb4_unicode_ci&quot;&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;      ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;    environment&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;      MYSQL_DATABASE&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;wordpress&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;      MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;password&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s self-explanatory enough — The &lt;code&gt;wp&lt;/code&gt; service is the actual WordPress site, and the &lt;code&gt;db&lt;/code&gt; service is a MySQL database (what we called an “external” dependency above). For the purposes of this example, this is all we need to get us to a point where you can just do a &lt;code&gt;docker-compose up&lt;/code&gt; to run the project.&lt;/p&gt;
&lt;p&gt;Now, you’ll notice that I didn’t provide a &lt;code&gt;Dockerfile&lt;/code&gt; — That’s because, in this instance, the official &lt;code&gt;wordpress&lt;/code&gt; image does all we need. For a more complex project, however (or if you wanted to deploy a Docker image to production, too), you’d probably want to write a Dockerfile that extends the official image.&lt;/p&gt;
&lt;h2 id=&quot;a-word-of-warning&quot;&gt;A Word of Warning&lt;/h2&gt;
&lt;p&gt;This is a bit of a tangent in the context of this article, but one thing about Docker that I &lt;em&gt;really&lt;/em&gt; need to mention, but nobody ever seems to talk about, is that the way you want to use Docker differs pretty significantly depending on whether you’re using it for development, or deploying Docker images to production.&lt;/p&gt;
&lt;p&gt;The main thing it comes down to is where your code is. Take the volume mounted onto the WordPress container in the example above:&lt;/p&gt;
&lt;pre class=&quot;astro-code Tomorrow Night&quot; style=&quot;background-color:#1D1F21;color:#C5C8C6; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CC6666&quot;&gt;volumes&lt;/span&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C5C8C6&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#B5BD68&quot;&gt;./wordpress:/var/www/html:rw&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The WordPress code on your host machine is mounted onto the container — Any changes you make to it will immediately be reflected in your running app.&lt;/p&gt;
&lt;p&gt;On the other hand, if you’re building an image to deploy to production, you &lt;em&gt;have&lt;/em&gt; to copy your code into your image at build time, so that it’s self-contained, and so that it can do anything it needs to do with your code at build time (building binaries, module bundling, etc.).&lt;/p&gt;
&lt;p&gt;I find it really important to point out this distinction whenever I get the chance to, because for some reason, a lot of high-profile tutorials out there will have you make a “development” Docker setup that has you rebuild your image every time you want it to reflect the changes in your code, even though this is totally impractical if you’re actually using Docker as your dev environment. I dread to think how many people have given up on using Docker for development because they found the idea of constantly rebuilding your app ridiculous (and rightfully so).&lt;/p&gt;
&lt;p&gt;Another thing worth mentioning is that, depending on your stack, you may need to have separate images for development and production (you can use &lt;a href=&quot;https://docs.docker.com/develop/develop-images/multistage-build/&quot;&gt;multi-stage builds&lt;/a&gt; to do this without having to maintain multiple Dockerfiles). PHP, for instance, doesn’t care — you’re using the exact same code and runtime in every environment. On the other hand, if you’re using something like a static site generator, or a language like Go that doesn’t have an external runtime, your production image is going to need to be much more lean than the one you use for development.&lt;/p&gt;
&lt;h2 id=&quot;automation-isnt-everything&quot;&gt;Automation Isn’t Everything&lt;/h2&gt;
&lt;p&gt;We’ve just spent a bunch of time talking about Docker and Docker Compose, and while containerizing your app absolutely makes a huge difference and is probably the single most helpful thing you can do to improve the onboarding experience, we should take a moment to remember that reducing your setup to a &lt;code&gt;docker-compose up&lt;/code&gt; isn’t the be-all and end-all of making your app friendly to work with.&lt;/p&gt;
&lt;p&gt;Here are a few other things to keep in mind.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your app or library has any sort of public API, you &lt;strong&gt;must&lt;/strong&gt; make it easy for people to see what it actually is; expecting people to dive into your code doesn’t count. For HTTP APIs, tools like &lt;a href=&quot;https://swagger.io/docs/specification/about/&quot;&gt;the OpenAPI spec&lt;/a&gt; and &lt;a href=&quot;https://swagger.io/tools/swagger-ui/&quot;&gt;Swagger UI&lt;/a&gt; make it easy to automatically generate API docs and expose them in a friendly way; use them.&lt;/li&gt;
&lt;li&gt;Even if you’ve simplified your dev environment setup and documented any public APIs you may have, there’s probably still a bunch of stuff you could put in your README. What is this app? Who’s it for? What standards/protocols does it implement? Is it a monorepo, or are there other projects one should look at to get the full picture? Hell, take a screenshot (if applicable) and put it at the top of the README. The more context people can glean from your docs, the better.&lt;/li&gt;
&lt;li&gt;If you can’t go the full-on containerization route for whatever reason (please make the effort to try, though!), make sure &lt;em&gt;all&lt;/em&gt; the steps one needs to go through to set up a dev environment for your app are well-documented. Don’t assume people will just know what to do with your code, even if your app uses a similar stack to other projects in your team or company.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;think-like-a-foss-maintainer&quot;&gt;Think Like a FOSS Maintainer&lt;/h2&gt;
&lt;p&gt;To conclude this post, I think the single most important piece of advice I can give on this topic is — &lt;strong&gt;Think like a FOSS maintainer&lt;/strong&gt;. Pretend for a second that you’re not developing an internal project that people are obligated to use, but a personal open-source project nobody will ever use if the DX is poor and the extent of your docs is a near-blank README.&lt;/p&gt;
&lt;p&gt;To an extent, you don’t even have to pretend — Yeah, of course you don’t have to sell people on your project if they’re obligated to work with it, but that doesn’t mean the atittude you take towards the stuff in this post doesn’t matter. Making the effort to make your coworkers’ jobs easier makes a huge difference in how they see you, your project, your team, and even affects the culture of the company as a whole.&lt;/p&gt;
&lt;p&gt;To be blunt, animosity between teams can grow from the most trivial of things; we have to make the extra effort to extend that olive branch when we can.&lt;/p&gt;
&lt;p&gt;The way I personally see this (with the disclaimer that this is absolutely my personal opinion — I can hardly pretend to speak for everyone) is that making sure your projects are well-documented is just a sign of basic respect towards your fellow devs. I wouldn’t dream of making someone bang their head against a wall trying to work on a project of mine just because I couldn’t be bothered to write proper docs.&lt;/p&gt;
&lt;h2 id=&quot;recap&quot;&gt;Recap&lt;/h2&gt;
&lt;p&gt;Let’s go over the important stuff one more time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#automate-automate-automate&quot;&gt;Do your best to automate your dev environments.&lt;/a&gt; This makes as much of a difference for you as it does for everyone else, especially if you switch between projects a lot.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#automation-isnt-everything&quot;&gt;Document your APIs.&lt;/a&gt; If you maintain an API other people are supposed to use, you can’t expect them to dive into your code to understand it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#automation-isnt-everything&quot;&gt;Put &lt;em&gt;everything&lt;/em&gt; in your READMEs.&lt;/a&gt; The more comprehensive your docs are, the less often you’ll have people tapping you on your shoulder to ask you things.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#think-like-a-foss-maintainer&quot;&gt;Think like a FOSS maintainer.&lt;/a&gt; Sell people on using your project, even if you don’t have to.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And, of course: Be excellent to each other.&lt;/p&gt;</content:encoded></item><item><title>Haxe + Flambe: An adventure</title><link>https://lynn.zone/blog/haxe-plus-flambe-an-adventure</link><guid isPermaLink="true">https://lynn.zone/blog/haxe-plus-flambe-an-adventure</guid><pubDate>Fri, 27 Mar 2015 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently built and released a small project, a web-based (HTML5 + Flash) game built using Haxe and Flambe.&lt;/p&gt;
&lt;p&gt;With the project as complete as it’ll ever be, I feel like sharing my experiences with the framework. For many reasons, it was an interesting framework to work with, and it brings some really fresh ideas to the table, but I don’t think I’ll be starting up another Flambe project soon. I outline why below.&lt;/p&gt;
&lt;h2 id=&quot;what-is-flambe&quot;&gt;What is Flambe?&lt;/h2&gt;
&lt;p&gt;Flambe is a cross-platform (but mainly web focused) gamedev framework built on top of Haxe, a language that compiles to everything under the sun.&lt;/p&gt;
&lt;p&gt;Now, Flambe is somewhat different in the world of Haxe-based frameworks in that it doesn’t use OpenFL (a Haxe framework that allows you to build Flash applications in Haxe and deploy them as native builds to pretty much anything) as part of the build process — rather, it rolls its own solution, limiting itself to only Flash and HTML5 as export options.&lt;/p&gt;
&lt;p&gt;I didn’t particularly care about this limitation, as I was building a web game anyway. However, those wanting to build a desktop or mobile game using the framework will probably want to look elsewhere, as you’ll be limited to AIR’s relatively poor performance on mobile (using the Flash target), as opposed to full native performance using OpenFL (which generates C++/SDL code).&lt;/p&gt;
&lt;h2 id=&quot;haxe-the-first-hurdle&quot;&gt;Haxe: The first hurdle&lt;/h2&gt;
&lt;p&gt;I have a love-hate relationship with Haxe. On one hand, I love that it lets me use a language structure I’m fairly familiar with (as a C#/Java dev) to write software for more-or-less anything with a processor. The whole concept is very intriguing to me. On the other hand, Haxe has one big problem: A lack of ecosystem, brought on by not being a very well known language.&lt;/p&gt;
&lt;p&gt;You see this problem manifest itself in many ways. Probably the biggest is the lack of a truly &lt;em&gt;good&lt;/em&gt; development environment. The only ‘real’ IDE Haxe has is FlashDevelop, which is Windows-only and simply isn’t very good. Contrast this to the IDEs available for C# (Visual Studio), Java (IntelliJ IDEA), Python (a number of IDEs, &lt;em&gt;including&lt;/em&gt; Visual Studio), JavaScript/CoffeeScript/TypeScript (WebStorm, among many others), and you see why I feel like a proper IDE is kind of a must in 2015.&lt;/p&gt;
&lt;p&gt;Now, this is a bit unfair to the Haxe mantainers — it isn’t their fault nobody has properly integrated a modern IDE with Haxe. But I do think it’s something people ought to consider, especially going into remotely complex projects.&lt;/p&gt;
&lt;p&gt;The second problem Haxe has is a lack of large community, and as such a lack of general-purpose libraries and ‘knowledge base’. Libraries I can do without, since I’m only doing gamedev anyway, and the basics are there — but if you find yourself stuck on some language quirk, the Haxe documentation itself is very barebones, and there’s no guarantee you’ll find your answer somewhere on the Web, like there usually is with popular languages. You may simply be left hanging to figure it out on your own, even if the problem is extremely simple.&lt;/p&gt;
&lt;p&gt;Someone once said that the only way to truly learn Haxe is to find someone who knows Haxe well and pester them until they’ve told you everything they know. I understand now how true this is.&lt;/p&gt;
&lt;h2 id=&quot;what-flambe-does-well&quot;&gt;What Flambe does well&lt;/h2&gt;
&lt;p&gt;There are a few features in Flambe that truly surprised me, especially in the context of an otherwise fairly barebones framework.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live asset reloading.&lt;/strong&gt; Change a file in an external editor and it updates in-game as soon as you hit save. This is incredibly useful for art, as you can see how it looks in the game without pressing a single button — It just works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Animations/tweening as a function of the framework itself.&lt;/strong&gt; Flambe makes a big deal out of most values being instances of Flambe’s ‘AnimatedFloat’ class, and it should. Any value that’s an AnimatedFloat (most properties of the Sprite class, for instance) can be tweened — with easing — by calling a single function.&lt;/p&gt;
&lt;p&gt;On the other hand, this isn’t &lt;em&gt;that&lt;/em&gt; big of a deal. You can get more or less the same functionality in other frameworks by using a third party tweening library, like LeanTween for Unity.&lt;/p&gt;
&lt;p&gt;**Scenes are incredibly versatile and are very easy to transition between. **Flambe’s scene transition framework blows other systems, like Unity’s, out of the water. In Unity, you might be able to replicate what Flambe does now that asynchronous loading no longer requires Unity Pro, but it would be a large undertaking to achieve a relatively minor effect.&lt;/p&gt;
&lt;p&gt;Want to make a scene that overlays the scene below it, and slide it in from off-screen? Flambe can do that in two lines of code. Want to keep the state of the last scene saved so you can transition back seamlessly? Flambe does that &lt;em&gt;by default&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-flambe-falls-short&quot;&gt;Where Flambe falls short&lt;/h2&gt;
&lt;p&gt;Despite being an interesting framework, I probably won’t be using it again in the foreseeable future, and I’d find it difficult to recommend to someone else — For a serious project, anyway. Here’s why.&lt;/p&gt;
&lt;p&gt;**Apparent disinterest in following industry standards. **Flambe packages both Flash and HTML5 builds as thin clients that grab assets from your server at runtime. This is great if you plan on hosting the games yourself, but the issue arises when you realize that it &lt;em&gt;doesn’t&lt;/em&gt; let you package a Flash build as a single SWF, which is how every single Flash portal expects you to package your game.&lt;/p&gt;
&lt;p&gt;I managed to work around this by using a fork of an older version of Flambe (4.0 if I remember correctly) that supports exporting a single SWF with an included preloader. But this obviously isn’t an ideal solution, and I’d be high and dry if that developer hadn’t been generous enough to release his fork.&lt;/p&gt;
&lt;p&gt;**Lack of support for fairly common features, while supporting more advanced features in their stead. **For instance, Flambe comes with &lt;a href=&quot;http://threerings.github.io/flump/&quot;&gt;Flump&lt;/a&gt; support out of the box, which is great! However, it leaves out support for traditional spritesheet and frame-based animations, which are much more standard and used by many, many more developers.&lt;/p&gt;
&lt;p&gt;Granted, frame-based animations are fairly trivial to implement — I implemented them into my game in less than an hour. But if anything, this only raises the question of why Flambe hasn’t done them from the beginning, let alone now.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So, is Flambe an &lt;em&gt;objectively bad&lt;/em&gt; framework? Of course not, no. There’s a reason it’s been used by companies like Nickelodeon and Disney, even over much more popular Haxe/web frameworks.&lt;/p&gt;
&lt;p&gt;However, should I find myself doing a similar project in the future, I’ll likely be using an OpenFL-based framework with a longer history and larger community like HaxeFlixel, or simply OpenFL itself (the API is fairly simple to work with, or so I’ve heard).&lt;/p&gt;
&lt;p&gt;If you’re planning on building a game for the purposes of a gamejam, or to publish on your own website, or for a client that doesn’t mind the limitations, I still encourage you to give Flambe a whirl. You may find that it’s exactly what you’re looking for.&lt;/p&gt;</content:encoded></item></channel></rss>