Skip to content

Commit

Permalink
feat: tracking events on trpc and api (#1136)
Browse files Browse the repository at this point in the history
* feat: tracking events on trpc and api

* fix: lock file

* chore: add noop

* chore: create middleware for modularity

* fix: env

* fix: revert middleware due to tsc ctx undefined

* chore: add env

* chore: remove unused env

* chore: missing env

* fix: test

* feat: add event props to api

* chore: blog post

* chore: update og image

* fix: invalid json body

* fix: typos
  • Loading branch information
mxkaske authored Dec 27, 2024
1 parent b71c81a commit 832b3a2
Show file tree
Hide file tree
Showing 71 changed files with 877 additions and 746 deletions.
3 changes: 3 additions & 0 deletions apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ TWILLIO_ACCOUNT_ID=your_account_id

SCREENSHOT_SERVICE_URL=http://your.endpoint
QSTASH_TOKEN=your_token

OPENPANEL_CLIENT_ID=
OPENPANEL_CLIENT_SECRET=
2 changes: 2 additions & 0 deletions apps/server/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ FLY_REGION=ams
RESEND_API_KEY=test
SQLD_HTTP_AUTH=basic:token
SCREENSHOT_SERVICE_URL=http://your.endpoint
OPENPANEL_CLIENT_ID=test
OPENPANEL_CLIENT_SECRET=test
2 changes: 0 additions & 2 deletions apps/server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export const env = createEnv({
UPSTASH_REDIS_REST_TOKEN: z.string().min(1),
FLY_REGION: z.enum(flyRegions),
CRON_SECRET: z.string(),
JITSU_WRITE_KEY: z.string().optional(),
JITSU_HOST: z.string().optional(),
SCREENSHOT_SERVICE_URL: z.string(),
QSTASH_TOKEN: z.string(),
NODE_ENV: z.string().default("development"),
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/v1/incidents/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { createRoute, z } from "@hono/zod-openapi";
import { and, db, eq } from "@openstatus/db";
import { incidentTable } from "@openstatus/db/src/schema/incidents";

import { Events } from "@openstatus/analytics";
import { HTTPException } from "hono/http-exception";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import type { incidentsApi } from "./index";
import { IncidentSchema, ParamsSchema } from "./schema";

Expand All @@ -13,6 +15,7 @@ const putRoute = createRoute({
tags: ["incident"],
description: "Update an incident",
path: "/:id",
middleware: [trackMiddleware(Events.UpdateIncident)],
request: {
params: ParamsSchema,
body: {
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { apiReference } from "@scalar/hono-api-reference";
import { cors } from "hono/cors";
import { logger } from "hono/logger";

import type { WorkspacePlan } from "@openstatus/db/src/schema";
import type { Limits } from "@openstatus/db/src/schema/plan/schema";
import { handleError, handleZodError } from "../libs/errors";
import { checkAPI } from "./check";
import { incidentsApi } from "./incidents";
import { middleware } from "./middleware";
import { secureMiddleware } from "./middleware";
import { monitorsApi } from "./monitors";
import { notificationsApi } from "./notifications";
import { pageSubscribersApi } from "./pageSubscribers";
Expand All @@ -20,6 +21,7 @@ export type Variables = {
workspaceId: string;
workspacePlan: {
title: "Hobby" | "Starter" | "Growth" | "Pro";
id: WorkspacePlan;
description: string;
price: number;
};
Expand Down Expand Up @@ -72,7 +74,7 @@ api.get(
/**
* Authentification Middleware
*/
api.use("/*", middleware);
api.use("/*", secureMiddleware);
api.use("/*", logger());

/**
Expand Down
39 changes: 38 additions & 1 deletion apps/server/src/v1/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { verifyKey } from "@unkey/api";
import type { Context, Next } from "hono";

import {
type EventProps,
parseInputToProps,
setupAnalytics,
} from "@openstatus/analytics";
import { db, eq } from "@openstatus/db";
import { selectWorkspaceSchema, workspace } from "@openstatus/db/src/schema";
import { getPlanConfig } from "@openstatus/db/src/schema/plan/utils";
import { HTTPException } from "hono/http-exception";
import { env } from "../env";
import type { Variables } from "./index";

export async function middleware(
export async function secureMiddleware(
c: Context<{ Variables: Variables }, "/*">,
next: Next,
) {
Expand All @@ -35,14 +40,46 @@ export async function middleware(
console.error("Workspace not found");
throw new HTTPException(401, { message: "Unauthorized" });
}

const _work = selectWorkspaceSchema.parse(_workspace);

c.set("workspacePlan", getPlanConfig(_workspace.plan));
c.set("workspaceId", `${result.ownerId}`);
c.set("limits", _work.limits);

await next();
}

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

// REMINDER: only track the event if the request was successful
if (!c.error) {
// We have checked the request to be valid already
let json: unknown;
if (c.req.raw.bodyUsed) {
try {
json = await c.req.json();
} catch {
json = {};
}
}
const additionalProps = parseInputToProps(json, eventProps);

// REMINDER: use setTimeout to avoid blocking the response
setTimeout(async () => {
const analytics = await setupAnalytics({
userId: `api_${c.get("workspaceId")}`,
workspaceId: c.get("workspaceId"),
plan: c.get("workspacePlan").id,
});
await analytics.track({ ...event, additionalProps });
}, 0);
}
};
}

/**
* TODO: move the plan limit into the Unkey `{ meta }` to avoid an additional db call.
* When an API Key is created, we need to include the `{ meta: { plan: "free" } }` to the key.
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/v1/monitors/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { createRoute, z } from "@hono/zod-openapi";
import { db, eq } from "@openstatus/db";
import { monitor } from "@openstatus/db/src/schema";

import { Events } from "@openstatus/analytics";
import { HTTPException } from "hono/http-exception";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import type { monitorsApi } from "./index";
import { ParamsSchema } from "./schema";

Expand All @@ -16,6 +18,7 @@ const deleteRoute = createRoute({
request: {
params: ParamsSchema,
},
middleware: [trackMiddleware(Events.DeleteMonitor)],
responses: {
200: {
content: {
Expand Down
14 changes: 3 additions & 11 deletions apps/server/src/v1/monitors/post.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { createRoute, z } from "@hono/zod-openapi";

import { trackAnalytics } from "@openstatus/analytics";
import { Events } from "@openstatus/analytics";
import { and, db, eq, isNull, sql } from "@openstatus/db";
import { monitor } from "@openstatus/db/src/schema";

import { HTTPException } from "hono/http-exception";
import { serialize } from "../../../../../packages/assertions/src";

import { getLimit } from "@openstatus/db/src/schema/plan/utils";
import { env } from "../../env";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import type { monitorsApi } from "./index";
import { MonitorSchema } from "./schema";
import { getAssertions } from "./utils";
Expand All @@ -19,6 +19,7 @@ const postRoute = createRoute({
tags: ["monitor"],
description: "Create a monitor",
path: "/",
middleware: [trackMiddleware(Events.CreateMonitor, ["url", "jobType"])],
request: {
body: {
description: "The monitor to create",
Expand Down Expand Up @@ -92,15 +93,6 @@ export function registerPostMonitor(api: typeof monitorsApi) {
})
.returning()
.get();
if (env.JITSU_WRITE_KEY) {
trackAnalytics({
event: "Monitor Created",
url: input.url,
periodicity: input.periodicity,
api: true,
workspaceId: String(workspaceId),
});
}

const data = MonitorSchema.parse(_newMonitor);

Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/v1/monitors/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { createRoute, z } from "@hono/zod-openapi";
import { and, db, eq } from "@openstatus/db";
import { monitor } from "@openstatus/db/src/schema";

import { Events } from "@openstatus/analytics";
import { HTTPException } from "hono/http-exception";
import { serialize } from "../../../../../packages/assertions/src/serializing";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import type { monitorsApi } from "./index";
import { MonitorSchema, ParamsSchema } from "./schema";
import { getAssertions } from "./utils";
Expand All @@ -15,6 +17,7 @@ const putRoute = createRoute({
tags: ["monitor"],
description: "Update a monitor",
path: "/:id",
middleware: [trackMiddleware(Events.UpdateMonitor)],
request: {
params: ParamsSchema,
body: {
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/v1/notifications/post.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createRoute } from "@hono/zod-openapi";

import { Events } from "@openstatus/analytics";
import { and, db, eq, inArray, isNull, sql } from "@openstatus/db";
import {
NotificationDataSchema,
Expand All @@ -11,6 +12,7 @@ import {
import { getLimit } from "@openstatus/db/src/schema/plan/utils";
import { HTTPException } from "hono/http-exception";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import type { notificationsApi } from "./index";
import { NotificationSchema } from "./schema";

Expand All @@ -19,6 +21,7 @@ const postRoute = createRoute({
tags: ["notification"],
description: "Create a notification",
path: "/",
middleware: [trackMiddleware(Events.CreateNotification)],
request: {
body: {
description: "The notification to create",
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/v1/pages/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { and, eq, inArray, isNull, sql } from "@openstatus/db";
import { db } from "@openstatus/db/src/db";
import { monitor, monitorsToPages, page } from "@openstatus/db/src/schema";

import { Events } from "@openstatus/analytics";
import { getLimit } from "@openstatus/db/src/schema/plan/utils";
import { HTTPException } from "hono/http-exception";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import { isNumberArray } from "../utils";
import type { pagesApi } from "./index";
import { PageSchema } from "./schema";
Expand All @@ -16,6 +18,7 @@ const postRoute = createRoute({
tags: ["page"],
description: "Create a status page",
path: "/",
middleware: [trackMiddleware(Events.CreatePage, ["slug"])],
request: {
body: {
description: "The status page to create",
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/v1/pages/put.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createRoute } from "@hono/zod-openapi";

import { Events } from "@openstatus/analytics";
import { and, eq, inArray, isNull, sql } from "@openstatus/db";
import { db } from "@openstatus/db/src/db";
import { monitor, monitorsToPages, page } from "@openstatus/db/src/schema";
import { HTTPException } from "hono/http-exception";
import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses";
import { trackMiddleware } from "../middleware";
import { isNumberArray } from "../utils";
import type { pagesApi } from "./index";
import { PageSchema, ParamsSchema } from "./schema";
Expand All @@ -14,6 +16,7 @@ const putRoute = createRoute({
tags: ["page"],
description: "Update a status page",
path: "/:id",
middleware: [trackMiddleware(Events.UpdatePage)],
request: {
params: ParamsSchema,
body: {
Expand Down
7 changes: 0 additions & 7 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ TINY_BIRD_API_KEY=tiny-bird-api-key
DATABASE_URL=file:./../../openstatus-dev.db
DATABASE_AUTH_TOKEN=any-token

# JITSU - no need to touch on local development
# JITSU_HOST="https://your-jitsu-domain.com"
# JITSU_WRITE_KEY="jitsu-key:jitsu-secret"

# Solves 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', see https://github.com/nextauthjs/next-auth/issues/3580
# NODE_TLS_REJECT_UNAUTHORIZED="0"

Expand Down Expand Up @@ -55,9 +51,6 @@ CRON_SECRET=

EXTERNAL_API_URL=

NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=

PLAYGROUND_UNKEY_API_KEY=

# RUM server with separate clickhouse for self-host
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
3 changes: 0 additions & 3 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@
"next-plausible": "3.12.4",
"next-themes": "0.2.1",
"nuqs": "2.2.3",
"posthog-js": "1.136.1",
"posthog-node": "4.0.1",
"random-word-slugs": "0.1.7",
"react": "19.0.0",
"react-day-picker": "8.10.1",
Expand Down Expand Up @@ -99,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.
8 changes: 6 additions & 2 deletions apps/web/src/app/(content)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export async function generateMetadata(props: {
}
const { title, publishedAt, description, slug, image } = post;

const encodedTitle = encodeURIComponent(title);
const encodedDescription = encodeURIComponent(description);
const encodedImage = encodeURIComponent(image);

return {
...defaultMetadata,
title,
Expand All @@ -42,7 +46,7 @@ export async function generateMetadata(props: {
url: `https://www.openstatus.dev/blog/${slug}`,
images: [
{
url: `https://openstatus.dev/api/og/post?title=${title}&image=${image}`,
url: `https://openstatus.dev/api/og/post?title=${encodedTitle}&image=${encodedImage}&description=${encodedDescription}`,
},
],
},
Expand All @@ -51,7 +55,7 @@ export async function generateMetadata(props: {
title,
description,
images: [
`https://openstatus.dev/api/og/post?title=${title}&image=${image}`,
`https://openstatus.dev/api/og/post?title=${encodedTitle}&image=${encodedImage}&description=${encodedDescription}`,
],
},
};
Expand Down
Loading

0 comments on commit 832b3a2

Please sign in to comment.