Skip to content
This repository was archived by the owner on Feb 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 55 additions & 60 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,67 @@
import { PluginEvent, PluginMeta, PluginAttachment } from '@posthog/plugin-scaffold'
import { Plugin } from '@posthog/plugin-scaffold'

declare var posthog: {
capture: (eventName: string, properties: Record<string,any>) => void
capture: (eventName: string, properties: Record<string, any>) => void
}

async function processEvent(event: PluginEvent, { storage }: PluginMeta) {
if (['session_started', 'session_ended'].includes(event.event)) {
return event
type SessionTrackerPlugin = Plugin<{
config: {
sessionLength: string
sessionStartEvent: string
sessionEndEvent: string
}

const THIRTY_MINUTES = 1000*60*30

// Last event by the user
const userLastSeen = await storage.get(`last_seen_${event.distinct_id}`, 0) as number

// Last registered session_start by the user
const userLastSessionStarted = await storage.get(`last_session_started_${event.distinct_id}`, 0) as number

let isFirstEventInSession = false

if (!event.properties) {
event['properties'] = {}
global: {
sessionLength: number
sessionStartEvent: string
sessionEndEvent: string
}
jobs: {
checkIfSessionIsOver: { distinct_id: string }
}
}>

const timestamp = event.timestamp

if (timestamp) {
const parsedTimestamp = new Date(timestamp).getTime()
const timeSinceLastSeen = parsedTimestamp - userLastSeen
isFirstEventInSession = timeSinceLastSeen > THIRTY_MINUTES

storage.set(`last_seen_${event.distinct_id}`, parsedTimestamp)

// If it's been over 30min since the user had an event, it's a new session
if (isFirstEventInSession) {
posthog.capture(
'session_started',
{
distinct_id: event.distinct_id,
time_since_last_seen: !userLastSeen ? 0 : timeSinceLastSeen,
// backdate to when session _actually_ started
timestamp: new Date(timestamp).toISOString(),
trigger_event: event.event
}
)
storage.set(`last_session_started_${event.distinct_id}`, parsedTimestamp)
export const setupPlugin: SessionTrackerPlugin['setupPlugin'] = ({ global, config }) => {
global.sessionLength = parseInt(config.sessionLength) || 30
global.sessionStartEvent = config.sessionStartEvent || 'Session start'
global.sessionEndEvent = config.sessionEndEvent || 'Session end'
}

// If we've started a new session, another session must have ended
if (userLastSessionStarted) {
posthog.capture(
'session_ended',
{
// backdate the session end to the timestamp last event before the new session
timestamp: new Date(userLastSeen).toISOString(),
distinct_id: event.distinct_id,
session_duration: userLastSeen - userLastSessionStarted
}
)
}
}

export const onEvent: SessionTrackerPlugin['onEvent'] = async (event, { cache, global, jobs }) => {
// skip this for the session start/end events
if (event.event === global.sessionStartEvent || event.event === global.sessionEndEvent) {
return
}
// check if we're the first one to increment this key in the last ${global.sessionLength} minutes
if ((await cache.incr(`session_${event.distinct_id}`)) === 1) {
// if so, dispatch a session start event
posthog.capture(global.sessionStartEvent, { distinct_id: event.distinct_id, timestamp: event.timestamp })
// and launch a job to check in 30min if the session is still alive
await jobs.checkIfSessionIsOver({ distinct_id: event.distinct_id }).runIn(global.sessionLength, 'minutes')
}
// make the key expire in ${global.sessionLength} min
await cache.expire(`session_${event.distinct_id}`, global.sessionLength * 60)
await cache.set(
`last_timestamp_${event.distinct_id}`,
event.timestamp || event.now || event.sent_at || new Date().toISOString()
)
}

event.properties['is_first_event_in_session'] = isFirstEventInSession
export const jobs: SessionTrackerPlugin['jobs'] = {
// a background job to check if a session is still in progress
checkIfSessionIsOver: async ({ distinct_id }, { jobs, cache, global }) => {
// check if there's a key that has not expired
const ping = await cache.get(`session_${distinct_id}`, undefined)
if (!ping) {
// if it expired, dispatch the session end event
const timestamp =
(await cache.get(`last_timestamp_${distinct_id}`, undefined)) ||
new Date(new Date().valueOf() - global.sessionLength * 60000).toISOString()

return event
await cache.set(`last_timestamp_${distinct_id}`, undefined)
posthog.capture(global.sessionEndEvent, { distinct_id, timestamp })
} else {
// if the key is still there, check again in a minute
await jobs.checkIfSessionIsOver({ distinct_id }).runIn(1, 'minute')
}
},
}

module.exports = {
processEvent,
}
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@
}
},
"devDependencies": {
"jest": "^26.6.3",
"posthog-plugins": "^0.2.2"
"jest": "^26.6.3"
},
"dependencies": {
"@posthog/plugin-scaffold": "^0.5.0",
"@posthog/plugin-scaffold": "^0.10.0",
"@types/jest": "^26.0.22",
"@types/node": "^14.14.41",
"ts-jest": "^26.5.5",
Expand Down
37 changes: 28 additions & 9 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
{
"name": "Session Tracker",
"url": "https://github.com/PostHog/session-tracker-plugin",
"description": "Advanced session tracking for PostHog.",
"main": "index.ts",
"config": [
{
"markdown": "## Important Note For Self-Hosted Users \nThis plugin leverages database storage to work. We estimate that it will use about 3MB of storage for every 10000 users tracked."
}
]
"name": "Session Tracker",
"url": "https://github.com/PostHog/session-tracker-plugin",
"description": "Advanced session tracking for PostHog.",
"main": "index.ts",
"posthogVersion": ">= 1.25.0",
"config": [
{
"key": "sessionLength",
"name": "Session length in minutes",
"type": "string",
"default": "30",
"required": true
},
{
"key": "sessionStartEvent",
"name": "Session start event",
"type": "string",
"default": "Session started",
"required": true
},
{
"key": "sessionEndEvent",
"name": "Session end event",
"type": "string",
"default": "Session ended",
"required": true
}
]
}
77 changes: 5 additions & 72 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -466,22 +466,10 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"

"@maxmind/geoip2-node@^2.3.1":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@maxmind/geoip2-node/-/geoip2-node-2.3.2.tgz#a0aaf8452693491e815021fe16e692959490ba6d"
integrity sha512-a1TSZt3uWe7yrgE2WMdTItK6XuG2Aj125cAXA+JuT2nomv3oqIK8XXg9iJVpzNXAYRV72XO0+zY+3HluOGgF3w==
dependencies:
camelcase-keys "^6.0.1"
ip6addr "^0.2.3"
lodash.set "^4.3.2"
maxmind "^4.2.0"

"@posthog/plugin-scaffold@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@posthog/plugin-scaffold/-/plugin-scaffold-0.5.0.tgz#0d46d0f39700ebf3cf613ba80a2c9a4dc6d13805"
integrity sha512-gE12hy/gYdMOYLP/PhbC2XfqvCeoXrdkFeR9HFfSK31RWU+aW+JMK7pi+CATatXfNpMCvjGxIYJk3QBPwm0WDA==
dependencies:
"@maxmind/geoip2-node" "^2.3.1"
"@posthog/plugin-scaffold@^0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@posthog/plugin-scaffold/-/plugin-scaffold-0.10.0.tgz#e80f57c5d3833753632607bed3ca71ebf272b12b"
integrity sha512-c8lNzQTBMJ0X3SCjcaD+mXZIx2thc+ptf8G4gbfT9YBFFD6TMaxs+/APMUab2aRJRbVwOsCGj9poxwuF4wxPpA==

"@sinonjs/commons@^1.7.0":
version "1.8.1"
Expand Down Expand Up @@ -901,15 +889,6 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==

camelcase-keys@^6.0.1:
version "6.2.2"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
dependencies:
camelcase "^5.3.1"
map-obj "^4.0.0"
quick-lru "^4.0.1"

camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
Expand Down Expand Up @@ -1657,14 +1636,6 @@ ip-regex@^2.1.0:
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=

ip6addr@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/ip6addr/-/ip6addr-0.2.3.tgz#660df0d27092434f0aadee025aba8337c6d7d4d4"
integrity sha512-qA9DXRAUW+lT47/i/4+Q3GHPwZjGt/atby1FH/THN6GVATA6+Pjp2nztH7k6iKeil7hzYnBwfSsxjthlJ8lJKw==
dependencies:
assert-plus "^1.0.0"
jsprim "^1.4.0"

is-accessor-descriptor@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
Expand Down Expand Up @@ -2343,7 +2314,7 @@ json5@^2.1.2:
dependencies:
minimist "^1.2.5"

jsprim@^1.2.2, jsprim@^1.4.0:
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
Expand Down Expand Up @@ -2407,11 +2378,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"

lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=

lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
Expand Down Expand Up @@ -2458,26 +2424,13 @@ map-cache@^0.2.2:
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=

map-obj@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7"
integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==

map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
dependencies:
object-visit "^1.0.0"

maxmind@^4.2.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.1.tgz#b20103f19e9fc12e4d1618814380d89a00f65770"
integrity sha512-0CxAgwWIwQy4zF1/qCMOeUPleMTYgfnsuIsZ4Otzx6hzON4PCqivPiH6Kz7iWrC++KOGCbSB3nxkJMvDEdWt6g==
dependencies:
mmdb-lib "1.2.0"
tiny-lru "7.0.6"

merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
Expand Down Expand Up @@ -2552,11 +2505,6 @@ mkdirp@1.x:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==

mmdb-lib@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mmdb-lib/-/mmdb-lib-1.2.0.tgz#0ecd93f4942f65a2d09be0502fa9126939606727"
integrity sha512-3XYebkStxqCgWJjsmT9FCaE19Yi4Tvs2SBPKhUks3rJJh52oF1AKAd9kei+LTutud3a6RCZ0o2Um96Fn7o3zVA==

ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
Expand Down Expand Up @@ -2814,11 +2762,6 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=

posthog-plugins@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/posthog-plugins/-/posthog-plugins-0.2.2.tgz#84bf2c7c50a3fea7a585187c9febda59ab774dd9"
integrity sha512-AFOKFexaCwKQ7+zjpM7g6Cj/75YF3QMx513xFahCHGAZrqrZuIPyY+xPvg1U4TA0J4P5OfzPz5tQYmPYcuppyw==

prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
Expand Down Expand Up @@ -2865,11 +2808,6 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==

quick-lru@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==

react-is@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
Expand Down Expand Up @@ -3366,11 +3304,6 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==

tiny-lru@7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24"
integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==

tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
Expand Down