Skip to content

Commit

Permalink
chore: blog post
Browse files Browse the repository at this point in the history
  • Loading branch information
mxkaske committed Dec 27, 2024
1 parent a03ea00 commit 0e18c38
Show file tree
Hide file tree
Showing 24 changed files with 259 additions and 214 deletions.
4 changes: 2 additions & 2 deletions apps/server/src/v1/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ export function trackMiddleware(event: EventProps, eventProps?: string[]) {
await next();

// REMINDER: only track the event if the request was successful
// REMINDER: use setTimeout to avoid blocking the response
if (c.finalized) {
if (!c.error) {
// We have checked the request to be valid already
const json = (await c.req.json()) as unknown;
const additionalProps = parseInputToProps(json, eventProps);

// REMINDER: use setTimeout to avoid blocking the response
setTimeout(async () => {
const analytics = await setupAnalytics({
userId: `api_${c.get("workspaceId")}`,
Expand Down
11 changes: 3 additions & 8 deletions apps/web/content-collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const prettyCode = [
dark: "github-dark-dimmed",
light: "github-light",
},
grid: true,
keepBackground: false,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
onVisitLine(node: any) {
// Prevent lines from collapsing in `display: grid` mode, and
Expand All @@ -36,14 +38,6 @@ const prettyCode = [
node.children = [{ type: "text", value: " " }];
}
},
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
onVisitHighlightedLine(node: any) {
node.properties.className.push("highlighted");
},
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
onVisitHighlightedWord(node: any) {
node.properties.className = ["word"];
},
},
];

Expand All @@ -61,6 +55,7 @@ const posts = defineCollection({
url: z.string().optional(),
avatar: z.string().optional(),
}),
tag: z.enum(["company", "engineering", "education"]),
}),
transform: async (document, context) => {
const mdx = await compileMDX(context, document, {
Expand Down
1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@
"postcss": "8.4.38",
"rehype-autolink-headings": "7.1.0",
"rehype-slug": "5.1.0",
"remark-gfm": "3.0.1",
"tailwindcss": "3.4.3",
"typescript": "5.6.2",
"unified": "10.1.2"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 10 additions & 6 deletions apps/web/src/components/content/article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Post } from "content-collections";
import Image from "next/image";
import Link from "next/link";

import { Avatar, AvatarFallback, AvatarImage } from "@openstatus/ui";
import { Avatar, AvatarFallback, AvatarImage, Badge } from "@openstatus/ui";

import { Mdx } from "@/components/content/mdx";
import { formatDate } from "@/lib/utils";
Expand Down Expand Up @@ -40,11 +40,15 @@ export function Article({ post }: { post: Post }) {
>
{post.author.name}
</Link>
<p>
{formatDate(post.publishedAt)}
<span className="mx-1 text-muted-foreground/70">&bull;</span>
{post.readingTime}
</p>
<div className="flex items-center gap-1.5 flex-wrap">
<time className="font-mono">{formatDate(post.publishedAt)}</time>
<span className="text-muted-foreground/70">&bull;</span>
<span className="font-mono">{post.readingTime}</span>
<span className="text-muted-foreground/70">&bull;</span>
<Badge variant="outline" className="font-normal capitalize">
{post.tag}
</Badge>
</div>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/content/mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function Mdx({ code, className }: MdxProps) {
// FIXME: weird behaviour when `prose-headings:font-cal` and on mouse movement font gets bigger
<div
className={cn(
"prose prose-slate dark:prose-invert prose-pre:my-0 prose-img:rounded-lg prose-pre:rounded-lg prose-img:border prose-pre:border prose-img:border-border prose-pre:border-border prose-headings:font-cal prose-headings:font-normal",
"prose prose-slate dark:prose-invert prose-pre:my-0 prose-img:rounded-lg prose-pre:bg-background prose-pre:rounded-lg prose-img:border prose-pre:border prose-img:border-border prose-pre:border-border prose-headings:font-cal prose-headings:font-normal",
className,
)}
>
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/content/pre.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client";

import { cn } from "@/lib/utils";
import { Button } from "@openstatus/ui/src/components/button";
import { Clipboard, ClipboardCopy } from "lucide-react";
import React from "react";
export interface PreProps extends React.HTMLAttributes<HTMLPreElement> {}

export default function Pre({ children, ...props }: PreProps) {
export default function Pre({ children, className, ...props }: PreProps) {
const [copied, setCopied] = React.useState(false);
const ref = React.useRef<HTMLPreElement>(null);

Expand Down Expand Up @@ -43,7 +44,7 @@ export default function Pre({ children, ...props }: PreProps) {
<ClipboardCopy className="h-5 w-5" />
)}
</Button>
<pre ref={ref} {...props}>
<pre ref={ref} className={cn("[&_code]:grid", className)} {...props}>
{children}
</pre>
</div>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/content/posts/2023-year-review.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2023-12-29
image: /assets/posts/2023-year-review/title.png
tag: company
---

It has been a wild six months for us at OpenStatus. In late June, we began
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/content/posts/dynamic-breadcrumb-nextjs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2024-08-19
image: /assets/posts/dynamic-breadcrumb-nextjs/breadcrumb.png
tag: engineering
---

In this post, we'll dive into the process of creating dynamic breadcrumbs in Next.js using parallel routes. Our goal is to build a breadcrumb component that automatically updates based on the current page and its hierarchy, all while leveraging server-side rendering for optimal performance.
Expand Down
195 changes: 195 additions & 0 deletions apps/web/src/content/posts/event-analytics-implementation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
---
title: How We Implemented Event Analytics with OpenPanel
description:
Leveraging Hono OpenAPI middleware and tRPC metadata + middleware to implement event analytics easily.
author:
name: Maximilian Kaske
url: https://x.com/mxkaske
avatar: /assets/authors/max.png
publishedAt: 2024-12-27
image: /assets/posts/event-analytics-implementation/event-analytics-implementation.png
tag: engineering
---

We had never really tracked events properly. We had some basic tracking in place, but it was not very useful. It is time to improve that with [OpenPanel](https://openpanel.dev?ref=openstatus).

After some research, we finally settled on leveraging tRPC and Hono middlewares. Shoutout to [Midday](https://midday.ai?ref=openstatus) for the (tRPC) inspiration. They use a similar approach with [next-safe-action](https://next-safe-action.dev?ref=openstatus) for their server actions.

This post is not a step-by-step guide but instead presents the core concepts and ideas behind the implementation. Please refer to the [Hono](https://hono.dev?ref=openstatus) or [tRPC](https://trpc.io?ref=openstatus) documentation for more detailed information and our [GitHub](https://openstatus.dev/github) repository for the full implementation.

---

First, let's start with the basics. We need to define the events we want to track, like `page_created`, `user_created`, etc.

```ts
// packages/analytics/src/events.ts
export type EventProps = {
name: string;
channel: string;
};

export const Events = {
CreatePage: {
name: "page_created",
channel: "page",
},
UpdatePage: {
name: "page_upated",
channel: "page",
},
// ... add more events
} as const satisfies Record<string, EventProps>;
```

Next, we need to initialize OpenPanel (see [installation](https://openpanel.dev/docs/sdks/javascript)) and set up the analytics in our application.

```ts
// packages/analytics/src/index.ts
import {
OpenPanel,
type PostEventPayload,
type IdentifyPayload,
} from "@openpanel/sdk";
import { type EventProps } from "@openstatus/analytics";

const op = new OpenPanel({
clientId: process.env.OPENPANEL_CLIENT_ID,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET,
});

export async function setupAnalytics(props: Partial<IdentifyPayload>) {
if (props.profileId) {
await op.identify(props):
}

return {
track: (opts: EventProps & PostEventPayload["properties"]) => {
const { name, ...rest } = opts;
return op.track(name, rest);
},
};
}
```

Now that we have the basic setup in place, we can start implementing the tracking in our application. We will use the tRPC middleware and metadata to track events. Below, we define the `trackEvent` middleware that will track the event after the procedure has been executed. An `enforceUserSession` middleware can be added to include the user's ID for tracking.

```ts {8,12,35} /trackEvent/
// packages/trpc/src/index.ts
import { after } from "next/server";
import { initTRPC } from "@trpc/server";
import { setupAnalytics, type EventProps } from "@openstatus/analytics";
import type { User } from "@openstatus/auth";

type Context = { user?: User };
type Meta = { track?: EventProps };

export const t = initTRPC
.context<Context>()
.meta<Meta>()
.create({ /* ... */ });


const trackEvent = t.middleware(async opts => {
const result = await opts.next(opts.ctx);

if (!result.ok) return result;

if (opts.meta.track) {
after(async () => {
const identify = opts.ctx.user ? { userId: opts.ctx.user.id } : {};
const analytics = await setupAnalytics(identify);
await analytics.track(opts.meta.track);
})
}
return result;
});

const enforceUserSession = t.middleware(async opts => {
// ... set user to ctx
});

export const protectedProcedure = t.procedure
.use(enforceUserSession)
.use(trackEvent);
```

The `after` function (similar to `waitUntil`) will execute the tracking after the procedure has been executed and won't block the response.

The `next()` return value has an `ok` boolean property to check if the procedure was successful. If not, we don't want to track the event.

How will we use it in a procedure? Adding a `meta` property will allow us to track the event by defining the event we want to track.

```ts {6}
// packages/trpc/src/router/page.ts
import { Events } from '@openstatus/analytics';
import { insertPageSchema } from "@openstatus/db";
import { createTRPCRouter, protectedProcedure } from "../trpc";

export const pageRouter = createTRPCRouter({
create: protectedProcedure
.meta({ track: Events.CreatePage })
.input(insertPageSchema)
.mutation(async (opts) => { /* ... */ })
});
```

Voilà! Each time you want to add tracking to a new procedure, you only need to add the `meta` property with the event you want to track. The middleware handles the rest.

---

Now, how do we track the events within our API? Let's start by adding the `trackMiddleware` function and only track the event if the response has been finalized.

```ts /trackMiddleware/
// app/server/src/middleware.ts
import { setupAnalytics, type EventProps } from "@openstatus/analytics";
import type { Context, Next } from "hono";
import type { User } from "@openstatus/auth";

export function trackMiddleware(event: EventProps) {
return async (c: Context<{ Variables: { user?: User } }, "/*">, next: Next) => {
await next();

if (!c.error) {
setTimeout(async () => {
const analytics = await setupAnalytics({
profileId: c.get("user")?.id,
});
await analytics.track(event);
}, 0);
}
};
}
```

Depending on where you are running the server, you might want to replace `setTimeout` with `waitUntil` (cf workers, Vercel) or other functions that extend the lifetime of the request without blocking the response.

And again, we check if there was an `error` before tracking the event. We don't want to track unsuccessful events.

The [`@hono/zod-openapi`](https://github.com/honojs/middleware/tree/main/packages/zod-openapi) routes have a `middleware` property that allows you to add middleware to the route. This is where we will add the tracking middleware.

```ts {11}
// apps/web/src/pages/post.ts
import { createRoute } from "@hono/zod-openapi";
import { Events } from "@openstatus/analytics";
import { trackMiddleware } from "../middleware";

const postRoute = createRoute({
method: "post",
tags: ["page"],
description: "Create a new Page",
path: "/",
middleware: [trackMiddleware(Events.CreatePage)],
request: { /* ... */ },
responses: { /* ... */},
});

// ...
```

---

And that's it. With minimal code changes and the help of middlewares, we have implemented event tracking in our application. You can swap [OpenPanel](https://openpanel.dev?ref=openstatus) with any other analytics provider like PostHog, but give it a try, it's an amazing tool!

By the way, this approach can be used for audit log tracking for example as well.

Check out our [GitHub](https://openstatus.dev/github) repository for the full implementation and don't forget to leave a star if you found this helpful.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2024-05-15
image: /assets/posts/migration-auth-clerk-to-next-auth/authjs.png
tag: engineering
---

We recently switched from [Clerk](https://clerk.com) to NextAuth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2023-10-29
image: /assets/posts/migration-backend-from-vercel-to-fly/fly.png
tag: company
---

In this article, we are going to see the reasons that made us change our backend
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/content/posts/migration-planetscale-to-turso.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2023-08-20
tag: company
---

## What are we building ? 🏗️
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2024-02-19
image: /assets/posts/monitoring-latency/all-hosting-providers.png
tag: education
---

> ⚠️ We are using the default settings for each provider and conducting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ description:
author:
name: Thibault Le Ouay Ducasse
url: https://bsky.app/profile/thibaultleouay.dev
avatar: /assets/authors/thibault.jpeg
publishedAt: 2024-03-14
image: /assets/posts/monitoring-vercel/serverless-vs-edge.png
tag: education
---

In our previous
Expand Down
Loading

0 comments on commit 0e18c38

Please sign in to comment.