@flags-sdk/posthog
Version:
PostHog adapter for the Flags SDK
1 lines • 13 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/provider/index.ts"],"names":["result"],"mappings":"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","sourcesContent":["import { PostHog } from 'posthog-node';\nimport type { PostHogAdapter, PostHogEntities, JsonType } from './types';\n\nexport { getProviderData } from './provider';\nexport type { PostHogEntities, JsonType };\n\nexport function createPostHogAdapter({\n postHogKey,\n postHogOptions,\n}: {\n postHogKey: ConstructorParameters<typeof PostHog>[0];\n postHogOptions: ConstructorParameters<typeof PostHog>[1];\n}): PostHogAdapter {\n const client = new PostHog(postHogKey, postHogOptions);\n\n const result: PostHogAdapter = {\n isFeatureEnabled: (options) => {\n return {\n async decide({ key, entities, defaultValue }): Promise<boolean> {\n const parsedEntities = parseEntities(entities);\n const result =\n (await client.isFeatureEnabled(\n trimKey(key),\n parsedEntities.distinctId,\n options,\n )) ?? defaultValue;\n if (result === undefined) {\n throw new Error(\n `PostHog Adapter isFeatureEnabled returned undefined for ${trimKey(key)} and no default value was provided.`,\n );\n }\n return result;\n },\n };\n },\n featureFlagValue: (options) => {\n return {\n async decide({ key, entities, defaultValue }) {\n const parsedEntities = parseEntities(entities);\n const flagValue = await client.getFeatureFlag(\n trimKey(key),\n parsedEntities.distinctId,\n options,\n );\n if (flagValue === undefined) {\n if (typeof defaultValue !== 'undefined') {\n return defaultValue;\n }\n throw new Error(\n `PostHog Adapter featureFlagValue found undefined for ${trimKey(key)} and no default value was provided.`,\n );\n }\n return flagValue;\n },\n };\n },\n featureFlagPayload: (getValue, options) => {\n return {\n async decide({ key, entities, defaultValue }) {\n const parsedEntities = parseEntities(entities);\n const payload = await client.getFeatureFlagPayload(\n trimKey(key),\n parsedEntities.distinctId,\n undefined,\n options,\n );\n if (!payload) {\n if (typeof defaultValue !== 'undefined') {\n return defaultValue;\n }\n throw new Error(\n `PostHog Adapter featureFlagPayload found undefined for ${trimKey(key)} and no default value was provided.`,\n );\n }\n return getValue(payload);\n },\n };\n },\n };\n\n return result;\n}\n\nfunction parseEntities(entities?: PostHogEntities): PostHogEntities {\n if (!entities) {\n throw new Error(\n 'PostHog Adapter: Missing entities, ' +\n 'flag must be defined with an identify() function.',\n );\n }\n return entities;\n}\n\nfunction assertEnv(name: string): string {\n const value = process.env[name];\n if (!value) {\n throw new Error(`PostHog Adapter: Missing ${name} environment variable`);\n }\n return value;\n}\n\n// Read until the first `.`\n// This supports defining multiple flags with the same key\n// Ex. with my-flag.is-enabled, my-flag.variant and my-flag.payload\nfunction trimKey(key: string): string {\n return key.split('.')[0] as string;\n}\n\nlet defaultPostHogAdapter: ReturnType<typeof createPostHogAdapter> | undefined;\nfunction getOrCreateDefaultAdapter() {\n if (!defaultPostHogAdapter) {\n defaultPostHogAdapter = createPostHogAdapter({\n postHogKey: assertEnv('NEXT_PUBLIC_POSTHOG_KEY'),\n postHogOptions: {\n host: assertEnv('NEXT_PUBLIC_POSTHOG_HOST'),\n personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY,\n featureFlagsPollingInterval: 10_000,\n // Presumption: Server IP is likely not a good proxy for user location\n disableGeoip: true,\n },\n });\n }\n return defaultPostHogAdapter;\n}\n\nexport const postHogAdapter: PostHogAdapter = {\n isFeatureEnabled: (...args) =>\n getOrCreateDefaultAdapter().isFeatureEnabled(...args),\n featureFlagValue: (...args) =>\n getOrCreateDefaultAdapter().featureFlagValue(...args),\n featureFlagPayload: (...args) =>\n getOrCreateDefaultAdapter().featureFlagPayload(...args),\n};\n","import type { FlagDefinitionsType, ProviderData } from 'flags';\n\n// See: https://posthog.com/docs/api/feature-flags#get-api-projects-project_id-feature_flags\ninterface ApiData {\n count: number;\n next?: string | null;\n previous?: string | null;\n results: {\n id: number;\n key: string;\n name: string;\n created_at: string;\n description: string;\n deleted: boolean;\n active: boolean;\n is_simple_flag: boolean;\n filters: {\n payloads?: Record<string, string>;\n multivariate?: Record<string, unknown>;\n };\n }[];\n}\n\nexport async function getProviderData(options: {\n personalApiKey: string;\n projectId: string;\n appHost?: string;\n}): Promise<ProviderData> {\n const hints: Exclude<ProviderData['hints'], undefined> = [];\n\n if (!options.personalApiKey) {\n hints.push({\n key: 'posthog/missing-personal-api-key',\n text: 'Missing PostHog Personal API Key',\n });\n }\n\n let host = options.appHost;\n if (!host) {\n try {\n host = getAppHost();\n } catch (e) {\n hints.push({\n key: 'posthog/missing-app-host',\n text: 'Missing NEXT_PUBLIC_POSTHOG_HOST environment variable',\n });\n }\n }\n\n if (!options.projectId) {\n hints.push({\n key: 'posthog/missing-project-id',\n text: 'Missing PostHog Project ID',\n });\n }\n\n if (hints.length > 0) {\n return { definitions: {}, hints };\n }\n\n const headers = {\n Authorization: `Bearer ${options.personalApiKey}`,\n };\n\n const res = await fetch(\n `${host}/api/projects/${options.projectId}/feature_flags`,\n {\n method: 'GET',\n headers,\n // @ts-expect-error used by some Next.js versions\n cache: 'no-store',\n },\n );\n\n if (res.status !== 200) {\n return {\n definitions: {},\n hints: [\n {\n key: `posthog/response-not-ok/${options.projectId}`,\n text: `Failed to fetch PostHog (Received ${res.status} response)`,\n },\n ],\n };\n }\n\n try {\n const data = (await res.json()) as ApiData;\n const items: ApiData['results'] = [...data.results];\n\n // paginate in a parallel request\n for (let offset = 100; offset < data.count; offset += 100) {\n const paginatedRes = await fetch(\n `${host}/api/projects/${options.projectId}/feature_flags?offset=${offset}&limit=100`,\n {\n method: 'GET',\n headers,\n // @ts-expect-error used by some Next.js versions\n cache: 'no-store',\n },\n );\n\n if (paginatedRes.status === 200) {\n const paginatedData = (await paginatedRes.json()) as ApiData;\n items.push(...paginatedData.results);\n } else {\n hints.push({\n key: `posthog/response-not-ok/${options.projectId}-${offset}`,\n text: `Failed to fetch PostHog (Received ${paginatedRes.status} response)`,\n });\n }\n }\n\n return {\n definitions: items.reduce<FlagDefinitionsType>((acc, item) => {\n acc[item.key] = {\n origin: `${host}/project/${options.projectId}/feature_flags/${item.id}`,\n description: item.name,\n createdAt: new Date(item.created_at).getTime(),\n options: !item.filters.payloads\n ? [{ value: false }, { value: true }]\n : Object.entries(item.filters.payloads ?? {}).map(\n ([key, value]) => ({\n value: JSON.parse(value),\n label: key,\n }),\n ),\n };\n return acc;\n }, {}),\n hints,\n };\n } catch (e) {\n return {\n definitions: {},\n hints: [\n {\n key: `posthog/response-not-ok/${options.projectId}`,\n text: 'Failed to fetch PostHog',\n },\n ],\n };\n }\n}\n\nexport const getAppHost = (apiHost?: string) => {\n const host = apiHost ?? process.env.NEXT_PUBLIC_POSTHOG_HOST;\n\n if (!host) {\n throw new Error('NEXT_PUBLIC_POSTHOG_HOST is not set');\n }\n\n if (host.includes('us.i.posthog.com')) {\n return 'https://us.posthog.com';\n }\n\n if (host.includes('eu.i.posthog.com')) {\n return 'https://eu.posthog.com';\n }\n\n return host;\n};\n"]}