Skip to content

Commit

Permalink
feat(tracking): add custom events with distinct id (#485)
Browse files Browse the repository at this point in the history
* add custom events with distinct posthog

* add step.meta.customEventName

* unify posthogClient

* refactor flowController.getMeta
  • Loading branch information
chohner authored Jan 4, 2024
1 parent d3fcf7d commit a479951
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 27 deletions.
11 changes: 3 additions & 8 deletions app/routes/action.send-rating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ import { json, redirect } from "@remix-run/node";
import { BannerState } from "~/components/UserFeedback";
import { userRatingFieldname } from "~/components/UserFeedback/RatingBox";
import { getSessionForContext } from "~/services/session";
import { PostHog } from "posthog-node";
import { config } from "~/services/env/web";
import { bannerStateName } from "~/services/feedback/handleFeedback";
import { getPosthogClient } from "~/services/analytics/posthogClient.server";

export const loader = () => redirect("/");

const { POSTHOG_API_KEY, POSTHOG_API_HOST, ENVIRONMENT } = config();
const posthogClient = POSTHOG_API_KEY
? new PostHog(POSTHOG_API_KEY, { host: POSTHOG_API_HOST })
: undefined;

export const action = async ({ request }: ActionFunctionArgs) => {
const { searchParams } = new URL(request.url);
const clientJavaScriptAvailable = searchParams.get("js") === "true";
Expand All @@ -37,8 +32,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {

const headers = { "Set-Cookie": await commitSession(session) };

posthogClient?.capture({
distinctId: ENVIRONMENT,
getPosthogClient()?.capture({
distinctId: config().ENVIRONMENT,
event: "rating given",
// eslint-disable-next-line camelcase
properties: { wasHelpful: userRatings[url], $current_url: url, context },
Expand Down
6 changes: 6 additions & 0 deletions app/routes/shared/step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { navItemsFromFlowSpecifics } from "~/services/flowNavigation";
import type { z } from "zod";
import type { CollectionSchemas } from "~/services/cms/schemas";
import { getButtonNavigationProps } from "~/util/getButtonNavigationProps";
import { sendCustomEvent } from "~/services/analytics/customEvent";

const structureCmsContent = (
formPageContent: z.infer<
Expand Down Expand Up @@ -221,6 +222,11 @@ export const action = async ({ params, request }: ActionFunctionArgs) => {
data: flowSession.data,
guards: flowSpecifics[flowId].guards,
});

const customEventName = flowController.getMeta(stepId)?.customEventName;
if (customEventName)
void sendCustomEvent(customEventName, validationResult.data, request);

return redirect(flowController.getNext(stepId).url, { headers });
};

Expand Down
33 changes: 33 additions & 0 deletions app/services/analytics/customEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { hasTrackingConsent } from "./gdprCookie.server";
import { config } from "../env/web";
import { parse } from "cookie";
import { getPosthogClient } from "./posthogClient.server";

function idFromCookie(request: Request) {
// Note: can't use cookie.parse(): https://github.com/remix-run/remix/discussions/5198
// Returns ENVIRONMENT if posthog's distinct_id can't be extracted
const { POSTHOG_API_KEY, ENVIRONMENT } = config();
if (!POSTHOG_API_KEY) return ENVIRONMENT;
const parsedCookie = parse(request.headers.get("Cookie") ?? "");
const phCookieString = parsedCookie[`ph_${POSTHOG_API_KEY}_posthog`] ?? "{}";
const phCookieObject = JSON.parse(phCookieString) as Record<string, string>;
return phCookieObject["distinct_id"] ?? ENVIRONMENT;
}

export async function sendCustomEvent(
eventName: string,
context: Record<string, any>,
request: Request,
) {
if (!(await hasTrackingConsent({ request }))) return;

getPosthogClient()?.capture({
distinctId: idFromCookie(request),
event: eventName,
properties: {
// eslint-disable-next-line camelcase
$current_url: new URL(request.url).pathname,
...context,
},
});
}
9 changes: 9 additions & 0 deletions app/services/analytics/posthogClient.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PostHog } from "posthog-node";
import { config } from "../env/web";

const { POSTHOG_API_KEY, POSTHOG_API_HOST } = config();
const posthogClient = POSTHOG_API_KEY
? new PostHog(POSTHOG_API_KEY, { host: POSTHOG_API_HOST })
: undefined;

export const getPosthogClient = () => posthogClient;
11 changes: 3 additions & 8 deletions app/services/feedback/handleFeedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,8 @@ import {
import { userRatingFieldname } from "~/components/UserFeedback/RatingBox";
import { validationError } from "remix-validated-form";
import { config } from "../env/web";
import { PostHog } from "posthog-node";
import { type Session, redirect } from "@remix-run/node";

const { POSTHOG_API_KEY, POSTHOG_API_HOST, ENVIRONMENT } = config();
const posthogClient = POSTHOG_API_KEY
? new PostHog(POSTHOG_API_KEY, { host: POSTHOG_API_HOST })
: undefined;
import { getPosthogClient } from "../analytics/posthogClient.server";

export const bannerStateName = "bannerState";

Expand All @@ -35,8 +30,8 @@ export const handleFeedback = async (formData: FormData, request: Request) => {
return validationError(result.error, result.submittedData);
}
bannerState[pathname] = BannerState.FeedbackGiven;
posthogClient?.capture({
distinctId: ENVIRONMENT,
getPosthogClient()?.capture({
distinctId: config().ENVIRONMENT,
event: "feedback given",
properties: {
wasHelpful: userRating[pathname],
Expand Down
20 changes: 9 additions & 11 deletions app/services/flow/buildFlowController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type StateMachine = ReturnType<
export type Config = MachineConfig<Context, any, StateMachineEvents>;
export type Guards = Record<string, (context: Context) => boolean>;
export type Meta = {
customEventName?: string;
progressPosition: number | undefined;
isUneditable: boolean | undefined;
done: (context: Context) => boolean | undefined;
Expand Down Expand Up @@ -70,18 +71,15 @@ export const buildFlowController = ({
const isInitialStepId = (currentStepId: string) =>
initialStepId === normalizeStepId(currentStepId);

const getMeta = (currentStepId: string): Meta | undefined =>
machine.getStateNodeByPath(normalizeStepId(currentStepId)).meta;

return {
getMeta: (currentStepId: string): Meta => {
return machine.getStateNodeByPath(normalizeStepId(currentStepId)).meta;
},
isDone: (currentStepId: string) => {
const meta: Meta = machine.getStateNodeByPath(currentStepId).meta;
return meta && "done" in meta && meta.done(context) === true;
},
isUneditable: (currentStepId: string) => {
const meta: Meta = machine.getStateNodeByPath(currentStepId).meta;
return meta && meta.isUneditable === true;
},
getMeta,
isDone: (currentStepId: string) =>
Boolean(getMeta(currentStepId)?.done(context)),
isUneditable: (currentStepId: string) =>
Boolean(getMeta(currentStepId)?.isUneditable),
getFlow: () => config,
isInitial: isInitialStepId,
isFinal: (currentStepId: string) =>
Expand Down

0 comments on commit a479951

Please sign in to comment.