Skip to content

Commit

Permalink
Posthog
Browse files Browse the repository at this point in the history
  • Loading branch information
robertherber committed Mar 27, 2024
1 parent 07b6c39 commit f1a115b
Show file tree
Hide file tree
Showing 19 changed files with 384 additions and 0 deletions.
Binary file modified bun.lockb
Binary file not shown.
6 changes: 6 additions & 0 deletions packages/posthog/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
node_modules
*.generated.ts
*.generated
.env
*.log
6 changes: 6 additions & 0 deletions packages/posthog/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"oven.bun-vscode",
"pucelle.run-on-save"
]
}
9 changes: 9 additions & 0 deletions packages/posthog/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"runOnSave.commands": [
{
"globMatch": "**/*.{graphql,test.ts}",
"command": "bun run graphql-codegen",
"runIn": "terminal"
},
]
}
13 changes: 13 additions & 0 deletions packages/posthog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# posthog

## Develop

```bash
bun dev
```

## Test

```bash
bun test
```
14 changes: 14 additions & 0 deletions packages/posthog/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PostHog } from 'posthog-node'

import plugin from './plugin'

const PH_HOST = plugin.config.PH_HOST ?? (plugin.config.PH_REGION === 'us' ? 'https://app.posthog.com' : 'https://eu.posthog.com')

export const client = new PostHog(
plugin.config.PH_API_KEY,
{ host: PH_HOST, ...plugin.config.postHogOptions },
)

await client.shutdown()

export default client
9 changes: 9 additions & 0 deletions packages/posthog/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import defaultConfig from '@zemble/graphql/codegen'

import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
...defaultConfig,
}

export default config
23 changes: 23 additions & 0 deletions packages/posthog/graphql/Mutation/randomNumber.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createTestApp } from '@zemble/core/test-utils'
import {
it, expect,
} from 'bun:test'

import plugin from '../../plugin'
import { graphql } from '../client.generated'

const randomNumberMutation = graphql(`
mutation RandomNumber {
randomNumber
}
`)

it('Should return a number', async () => {
const app = await createTestApp(plugin)

const response = await app.gqlRequest(randomNumberMutation, {})
expect(response.data).toEqual({

randomNumber: expect.any(Number),
})
})
11 changes: 11 additions & 0 deletions packages/posthog/graphql/Mutation/randomNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { MutationResolvers } from '../schema.generated'

const randomNumber: MutationResolvers['randomNumber'] = (_, __, { pubsub }) => {
const randomNumber = Math.floor(Math.random() * 1000)

pubsub.publish('randomNumber', randomNumber)

return randomNumber
}

export default randomNumber
21 changes: 21 additions & 0 deletions packages/posthog/graphql/Query/hello.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createTestApp } from '@zemble/core/test-utils'
import { it, expect } from 'bun:test'

import plugin from '../../plugin'
import { graphql } from '../client.generated'

const HelloWorldQuery = graphql(`
query Hello {
hello
}
`)

it('Should return world!', async () => {
const app = await createTestApp(plugin)

const response = await app.gqlRequest(HelloWorldQuery, {})

expect(response.data).toEqual({
hello: 'world!',
})
})
5 changes: 5 additions & 0 deletions packages/posthog/graphql/Query/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { QueryResolvers } from '../schema.generated'

const hello: QueryResolvers['hello'] = () => 'world!'

export default hello
20 changes: 20 additions & 0 deletions packages/posthog/graphql/Subscription/countdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { SubscriptionResolvers } from '../schema.generated'

const countdown: SubscriptionResolvers['countdown'] = {
// This will return the value on every 1 sec until it reaches 0
// eslint-disable-next-line object-shorthand
subscribe: async function* (_, { from }, { logger }) {
// eslint-disable-next-line no-plusplus
for (let i = from; i >= 0; i--) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
logger.info('countdown', { countdown: i })
setTimeout(resolve, 1000)
})
yield { countdown: i }
}
},
resolve: (payload: unknown) => (payload as { readonly countdown: number}).countdown,
}

export default countdown
15 changes: 15 additions & 0 deletions packages/posthog/graphql/Subscription/randomNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { SubscriptionResolvers } from '../schema.generated'

const randomNumber: SubscriptionResolvers['randomNumber'] = {
// subscribe to the randomNumber event
subscribe: (_, __, { pubsub }) => {
console.log('subscribing to randomNumber')
return pubsub.subscribe('randomNumber')
},
resolve: (payload: number) => {
console.log('resolving randomNumber', payload)
return payload
},
}

export default randomNumber
22 changes: 22 additions & 0 deletions packages/posthog/graphql/Subscription/tick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { SubscriptionResolvers } from '../schema.generated'

let initialized = false
const initializeOnce = (pubsub: Zemble.PubSubType) => {
if (initialized) return
initialized = true
setInterval(() => {
pubsub.publish('tick', Date.now())
}, 1000)
}

const tick: SubscriptionResolvers['tick'] = {
// subscribe to the tick event
subscribe: (_, __, { pubsub }) => {
initializeOnce(pubsub)
console.log('subscribing to tick')
return pubsub.subscribe('tick')
},
resolve: (payload: number) => payload,
}

export default tick
13 changes: 13 additions & 0 deletions packages/posthog/graphql/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Query {
hello: String!
}

type Mutation {
randomNumber: Int!
}

type Subscription {
countdown(from: Int!): Int!
tick: Float!
randomNumber: Int!
}
38 changes: 38 additions & 0 deletions packages/posthog/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "posthog",
"version": "0.0.1",
"description": "",
"type": "module",
"keywords": [
"zemble",
"zemble-plugin",
"@zemble"
],
"dependencies": {
"@zemble/bun": "workspace:*",
"@zemble/core": "workspace:*",
"@zemble/graphql": "workspace:*",
"@zemble/routes": "workspace:*",
"posthog-node": "^4.0.0"
},
"scripts": {
"test": "bun test",
"dev": "zemble-dev plugin.ts",
"typecheck": "tsc --noEmit",
"codegen": "graphql-codegen"
},
"devDependencies": {
"@types/bun": "*",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-resolvers": "^4.0.1",
"@tsconfig/bun": "^1.0.1"
},
"peerDependencies": {
"typescript": "^5.3.3"
},
"module": "plugin.ts",
"main": "plugin.ts"
}
141 changes: 141 additions & 0 deletions packages/posthog/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Plugin } from '@zemble/core'
import GraphQL from '@zemble/graphql'
import Routes from '@zemble/routes'
// import https from 'node:https'
import * as tls from 'node:tls'

import type { PostHogOptions } from 'posthog-node'

type PhRegion = 'us' | 'eu'

interface Config extends Zemble.GlobalConfig {
readonly PH_API_KEY: string
readonly PH_REGION?: PhRegion
readonly PH_HOST?: string,
readonly PH_API_HOST?: string,
readonly PH_ASSET_HOST?: string,
readonly PH_INGEST_PATH?: string

readonly postHogOptions?: PostHogOptions
}

const defaultConfig = {
PH_REGION: process.env.PH_REGION as PhRegion ?? 'us',
PH_HOST: process.env.PH_HOST,
PH_API_KEY: process.env.PH_API_KEY ?? '',
PH_API_HOST: process.env.PH_API_HOST,
PH_ASSET_HOST: process.env.PH_ASSET_HOST,
PH_INGEST_PATH: process.env.PH_INGEST_PATH ?? '/ph-ingest',
} satisfies Config

// const agent = new https.Agent({
// rejectUnauthorized: false,
// })

export default new Plugin<Config, typeof defaultConfig>(
import.meta.dir,
{
defaultConfig,
middleware: async ({ app, config, logger }) => {
const {
PH_REGION, PH_API_KEY, PH_API_HOST, PH_ASSET_HOST, PH_INGEST_PATH,
} = config

if (!PH_API_KEY || PH_API_KEY === '') {
throw new Error('PH_API_KEY not set')
}

if (!['us', 'eu'].includes(PH_REGION)) {
logger.warn('Expected PH_REGION to be either "us" or "eu"')
}

const phHost = PH_API_HOST ?? `${PH_REGION}.i.posthog.com`
const phAssetHost = PH_ASSET_HOST ?? `${PH_REGION}-assets.i.posthog.com`

console.log('phHost', phHost)

app.hono.all(`${PH_INGEST_PATH}/static/*`, async (ctx) => {
const path = ctx.req.path.replace(PH_INGEST_PATH, '')

console.log('path', path)
console.log('headers', ctx.req.header())

const res = await fetch(`https://${phAssetHost}${path}`, {
method: ctx.req.method,
body: ctx.req.raw.body,
headers: {
...ctx.req.header(),
host: phAssetHost.replace('https://', ''),
},
/* tls: {
rejectUnauthorized: false,
checkServerIdentity: (hostname: string, cert: tls.PeerCertificate): Error | undefined => {
console.log('checkServerIdentity', hostname)
if (hostname.includes('posthog.com')) {
return undefined
}
/// here you can add a custom check for specific cert and/or hostname
return tls.checkServerIdentity(hostname, cert)
},
},
cache: 'no-cache', */
}).catch((err) => {
console.error(err)
throw err
})

const resHeaders = res.headers.toJSON()

console.log('resHeaders', resHeaders)
delete resHeaders['content-encoding']
return ctx.newResponse(res.body, res.status as 200, resHeaders)
})

app.hono.all(`${PH_INGEST_PATH}/*`, async (ctx) => {
const url = new URL(ctx.req.url)
// eslint-disable-next-line functional/immutable-data
url.pathname = url.pathname.replace(PH_INGEST_PATH, '')
// eslint-disable-next-line functional/immutable-data
url.host = phHost
url.port = '443'
// eslint-disable-next-line functional/immutable-data
url.protocol = 'https:'

// console.log('url.host', url.host.toString())
console.log('url', url.toString())
console.log('data', await ctx.req.text())

const res = await fetch(url, {
method: ctx.req.method,
body: ctx.req.raw.body,
headers: {
...ctx.req.header(),
host: phHost.replace('https://', ''),
},
/* tls: {
rejectUnauthorized: false,
checkServerIdentity: (hostname: string, cert: tls.PeerCertificate): Error | undefined => {
console.log('checkServerIdentity', hostname)
if (hostname.includes('posthog.com')) {
return undefined
}
/// here you can add a custom check for specific cert and/or hostname
return tls.checkServerIdentity(hostname, cert)
},
},
cache: 'no-cache', */
}).catch((err) => {
console.error(url.toString(), err)
throw err
})

const resHeaders = res.headers.toJSON()

console.log(`${url.toString()}: ${res.statusText}`, await res.text())
delete resHeaders['content-encoding']
return ctx.newResponse(res.body, res.status as 200, resHeaders)
})
},
dependencies: [{ plugin: GraphQL }, { plugin: Routes }],
},
)
Loading

0 comments on commit f1a115b

Please sign in to comment.