Skip to content

Commit

Permalink
feat: added posthog feature flags instead of split
Browse files Browse the repository at this point in the history
  • Loading branch information
meza committed Apr 3, 2023
1 parent 2c98e53 commit 2577d89
Show file tree
Hide file tree
Showing 18 changed files with 88 additions and 107 deletions.
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,5 @@ MIXPANEL_TOKEN=yetanothersupersecret
NODE_ENV=development # required for architect
SENTRY_DSN=https://example.dsn/for/sentry
SESSION_SECRET=secret
SPLIT_DEBUG=false
SPLIT_SERVER_TOKEN=localhost # this one is actually what it should be on your local machine
POSTHOG_TOKEN=phc_token
POSTHOG_API=https://eu.posthog.com
10 changes: 0 additions & 10 deletions devFeatures.yml

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"jose": "4.13.1",
"js-cookie": "3.0.1",
"posthog-js": "^1.51.5",
"posthog-node": "^2.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "12.2.0",
Expand Down
1 change: 0 additions & 1 deletion src/components/ExposeAppConfig/ExposeAppConfig.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ describe('ExposeAppConfig', () => {
googleAnalyticsId: 'ga-id',
visitorId: 'a-visitor-id',
isProduction: true,
splitToken: 'a-split-token',
cookieYesToken: 'a-cookieyes-token',
version: '0.0.0-dev',
sentryDsn: 'a-sentry-dsn',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`ExposeAppConfig > can expose the app config correctly 1`] = `
<script
dangerouslySetInnerHTML={
{
"__html": "window.appConfig = {\\"hotjarId\\":\\"a-hotjar-id\\",\\"googleAnalyticsId\\":\\"ga-id\\",\\"visitorId\\":\\"a-visitor-id\\",\\"isProduction\\":true,\\"splitToken\\":\\"a-split-token\\",\\"cookieYesToken\\":\\"a-cookieyes-token\\",\\"version\\":\\"0.0.0-dev\\",\\"sentryDsn\\":\\"a-sentry-dsn\\",\\"posthogApi\\":\\"a-posthog-api\\",\\"posthogToken\\":\\"a-posthog-token\\"}",
"__html": "window.appConfig = {\\"hotjarId\\":\\"a-hotjar-id\\",\\"googleAnalyticsId\\":\\"ga-id\\",\\"visitorId\\":\\"a-visitor-id\\",\\"isProduction\\":true,\\"cookieYesToken\\":\\"a-cookieyes-token\\",\\"version\\":\\"0.0.0-dev\\",\\"sentryDsn\\":\\"a-sentry-dsn\\",\\"posthogApi\\":\\"a-posthog-api\\",\\"posthogToken\\":\\"a-posthog-token\\"}",
}
}
id="app-config"
Expand Down
4 changes: 2 additions & 2 deletions src/features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ describe('The feature flags', () => {
`);
expect(Object.values(Features)).toMatchInlineSnapshot(`
[
"auth_enabled",
"hello_split",
"auth",
"hello",
]
`);
});
Expand Down
8 changes: 4 additions & 4 deletions src/features.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* This file contains the list of feature flags that are used in the application.
* Please keep this file in sync with the feature flags in the Split.io dashboard.
* Please keep this file in sync with the feature flags in the posthog.com dashboard.
* Also keep them sorted alphabetically.
*
* @see {@link https://app.split.io/}
* @see {@link https://posthog.com/docs/feature-flags/manual}
*/
export enum Features {
AUTH = 'auth_enabled',
HELLO = 'hello_split'
AUTH = 'auth',
HELLO = 'hello'
}
35 changes: 24 additions & 11 deletions src/hooks/hasFeature.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { hasFeature } from '~/hooks/hasFeature';
import { posthog } from '~/posthog.server';
import { getVisitorIdFromRequest } from '~/session.server';
import splitClient from '~/split.server';

vi.mock('~/split.server');
vi.mock('~/posthog.server', () => ({
posthog: {
isFeatureEnabled: vi.fn()
}
}));

vi.mock('~/session.server');
describe('The hasFeature hook', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(splitClient.ready).mockResolvedValue();
});

it('should return true for an on flag', async () => {
const request = new Request('https://example.com');
const feature = 'a-feature';

vi.mocked(getVisitorIdFromRequest).mockResolvedValueOnce('visitorId');
vi.mocked(splitClient.getTreatment).mockReturnValue('on');
vi.mocked(posthog.isFeatureEnabled).mockResolvedValueOnce(true);
const result = await hasFeature(request, feature as never);

expect(result).toBeTruthy();

expect(splitClient.ready).toBeCalledTimes(1);
expect(getVisitorIdFromRequest).toBeCalledTimes(1);
expect(splitClient.getTreatment).toBeCalledTimes(1);
expect(splitClient.getTreatment).toHaveBeenCalledWith('visitorId', feature);
expect(getVisitorIdFromRequest).toHaveBeenCalledWith(request);

});
Expand All @@ -33,16 +34,28 @@ describe('The hasFeature hook', () => {
const request = new Request('https://example.com');
const feature = 'another-feature';

vi.mocked(posthog.isFeatureEnabled).mockResolvedValueOnce(false);
vi.mocked(getVisitorIdFromRequest).mockResolvedValueOnce('visitorId2');
vi.mocked(splitClient.getTreatment).mockReturnValue('off');
const result = await hasFeature(request, feature as never);

expect(result).toBeFalsy();

expect(splitClient.ready).toBeCalledTimes(1);
expect(getVisitorIdFromRequest).toBeCalledTimes(1);
expect(splitClient.getTreatment).toBeCalledTimes(1);
expect(splitClient.getTreatment).toHaveBeenCalledWith('visitorId2', feature);
expect(getVisitorIdFromRequest).toHaveBeenCalledWith(request);

});

it('should return false for an undefined flag', async () => {
const request = new Request('https://example.com');
const feature = 'another-feature';

vi.mocked(posthog.isFeatureEnabled).mockResolvedValueOnce(undefined);
vi.mocked(getVisitorIdFromRequest).mockResolvedValueOnce('visitorId3');
const result = await hasFeature(request, feature as never);

expect(result).toBeFalsy();

expect(getVisitorIdFromRequest).toBeCalledTimes(1);
expect(getVisitorIdFromRequest).toHaveBeenCalledWith(request);

});
Expand Down
8 changes: 3 additions & 5 deletions src/hooks/hasFeature.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { posthog } from '~/posthog.server';
import { getVisitorIdFromRequest } from '~/session.server';
import splitClient from '~/split.server';
import type { Features } from '~/features';

/**
* @name hasFeature
* @description
*/
export const hasFeature = async (request: Request, feature: Features): Promise<boolean> => {
await splitClient.ready();
const visitorId = await getVisitorIdFromRequest(request);
const serverTreatment = splitClient.getTreatment(visitorId, feature);

return serverTreatment === 'on';
const isEnabled = await posthog.isFeatureEnabled(feature, visitorId);
return !!isEnabled;
};

34 changes: 34 additions & 0 deletions src/posthog.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PostHog } from 'posthog-node';
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('posthog-node', () => ({
PostHog: vi.fn()
}));

describe('The PostHog Server', () => {
beforeEach(() => {
vi.resetAllMocks();
});

it('should initialise the posthog server', async () => {
vi.stubEnv('POSTHOG_TOKEN', 'test-token');
vi.stubEnv('POSTHOG_API', 'test-api');
const processSpy = vi.spyOn(process, 'on');
const shutdownStub = vi.fn();
const expected = {
shutdownAsync: shutdownStub
} as never;

vi.mocked(PostHog).mockImplementation(() => expected);

const { posthog } = await import('~/posthog.server');
expect(PostHog).toHaveBeenCalledWith('test-token', { host: 'test-api' });

const exitCallback = processSpy.mock.calls[0][1];
await exitCallback();

expect(shutdownStub).toHaveBeenCalled();

expect(posthog).toEqual(expected);
});
});
12 changes: 12 additions & 0 deletions src/posthog.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PostHog } from 'posthog-node';

export const posthog = new PostHog(
process.env.POSTHOG_TOKEN,
{
host: process.env.POSTHOG_API
}
);

process.on('exit', async () => {
await posthog.shutdownAsync();
});
10 changes: 0 additions & 10 deletions src/root.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useChangeLanguage } from '~/hooks/useChangeLanguage';
import { remixI18next } from '~/i18n';
import { createUserSession } from '~/session.server';
import splitClient from '~/split.server';
import App, { handle, links, loader, meta } from './root';

vi.mock('~/session.server');
vi.mock('~/i18n');
vi.mock('~/split.server');
vi.mock('./styles/app.css', () => ({ default: 'app.css' }));
vi.mock('./styles/dark.css', () => ({ default: 'dark.css' }));
vi.mock('./styles/light.css', () => ({ default: 'light.css' }));
Expand Down Expand Up @@ -99,15 +97,12 @@ describe('The root module', () => {
} as never
});
vi.mocked(remixI18next.getLocale).mockResolvedValue('en');
vi.mocked(splitClient.ready).mockResolvedValue();
vi.mocked(splitClient.track).mockReturnValue(true);

vi.stubEnv('NODE_ENV', 'development');
vi.stubEnv('GOOGLE_ANALYTICS_ID', 'ga-id');
vi.stubEnv('HOTJAR_ID', 'a-hotjar-id');
vi.stubEnv('MIXPANEL_TOKEN', 'a-mixpanel-token');
vi.stubEnv('MIXPANEL_API', 'a-mixpanel-api');
vi.stubEnv('SPLIT_CLIENT_TOKEN', 'a-split-token');
vi.stubEnv('COOKIEYES_TOKEN', 'a-cookieyes-token');

});
Expand All @@ -123,7 +118,6 @@ describe('The root module', () => {
"googleAnalyticsId": "ga-id",
"hotjarId": "a-hotjar-id",
"isProduction": false,
"splitToken": "a-split-token",
"version": "0.0.0-dev",
"visitorId": "a-visitorId",
},
Expand All @@ -150,7 +144,6 @@ describe('The root module', () => {
"googleAnalyticsId": "ga-id",
"hotjarId": "a-hotjar-id",
"isProduction": true,
"splitToken": "a-split-token",
"version": "0.0.0-dev",
"visitorId": "a-visitorId",
},
Expand All @@ -165,8 +158,6 @@ describe('The root module', () => {

expect(createUserSession).toHaveBeenCalledWith(request);
expect(remixI18next.getLocale).toHaveBeenCalledWith(request);
expect(splitClient.ready).toHaveBeenCalled();
expect(splitClient.track).toHaveBeenCalledWith('a-visitorId', 'anonymous', 'page_view');

});
});
Expand All @@ -177,7 +168,6 @@ describe('The root module', () => {
googleAnalyticsId: 'ga-id',
visitorId: 'a-visitor-id',
isProduction: true,
splitToken: 'a-split-token',
cookieYesToken: 'a-cookieyes-token',
version: '0.0.0-dev',
sentryDsn: 'a-sentry-dsn',
Expand Down
6 changes: 1 addition & 5 deletions src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { useChangeLanguage } from '~/hooks/useChangeLanguage';
import { remixI18next } from '~/i18n';
import { defaultNS } from '~/i18n/i18n.config';
import { createUserSession } from '~/session.server';
import splitClient from '~/split.server';
import styles from './styles/app.css';
import type { LinksFunction, LoaderFunction, MetaFunction } from '@remix-run/node';
import type { ColorMode } from '~/components/ColorModeSwitcher';
Expand All @@ -40,15 +39,12 @@ export const loader: LoaderFunction = async ({ request }) => {
const [locale, packageJson, cookieData] = await Promise.all([
remixI18next.getLocale(request),
import('../package.json'),
createUserSession(request),
splitClient.ready()
createUserSession(request)
]);
splitClient.track(cookieData.visitorId, 'anonymous', 'page_view');
return json({
appConfig: {
googleAnalyticsId: process.env.GOOGLE_ANALYTICS_ID,
hotjarId: process.env.HOTJAR_ID,
splitToken: process.env.SPLIT_CLIENT_TOKEN,
cookieYesToken: process.env.COOKIEYES_TOKEN,
isProduction: process.env.NODE_ENV === 'production',
visitorId: cookieData.visitorId,
Expand Down
4 changes: 3 additions & 1 deletion src/routes/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ vi.mock('~/components/Hello', () => ({
Hello: () => '<Hello />',
links: () => ['hello-links']
}));
vi.mock('~/hooks/hasFeature');
vi.mock('~/hooks/hasFeature', () => ({
hasFeature: vi.fn()
}));
vi.mock('@remix-run/react');
vi.mock('@remix-run/node');

Expand Down
4 changes: 3 additions & 1 deletion src/routes/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { hasFeature } from '~/hooks/hasFeature';
import login, { loader } from './login';

vi.mock('~/components/Login');
vi.mock('~/hooks/hasFeature');
vi.mock('@remix-run/node');
vi.mock('~/hooks/hasFeature', () => ({
hasFeature: vi.fn()
}));

const redirectError = 'redirect was called';
describe('The Login Route', () => {
Expand Down
38 changes: 0 additions & 38 deletions src/split.server.test.ts

This file was deleted.

14 changes: 0 additions & 14 deletions src/split.server.ts

This file was deleted.

2 changes: 0 additions & 2 deletions types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ declare global {
interface AppConfig {
googleAnalyticsId: string;
hotjarId: string;
splitToken: string;
cookieYesToken: string;
isProduction: boolean;
visitorId: string;
Expand All @@ -30,7 +29,6 @@ declare global {
HOTJAR_ID: string;
NODE_ENV: string;
SESSION_SECRET: string | undefined
SPLIT_SERVER_TOKEN: string;
I18N_DEBUG: string;
SENTRY_DSN: string;
POSTHOG_TOKEN: string;
Expand Down

0 comments on commit 2577d89

Please sign in to comment.