feat: Add an analytics module and local and posthog providers (#12505)

* feat: Add an analytics module and local and posthog providers

* fix: Add tests and wire up in missing places

* fix: Address feedback and add missing module typing

* fix: Address feedback and add missing module typing

---------

Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Stevche Radevski
2025-05-19 19:57:13 +02:00
committed by GitHub
co-authored by Adrien de Peretti Oli Juhl
parent 52bd9f9a53
commit b9a51e217d
49 changed files with 1009 additions and 0 deletions
@@ -0,0 +1 @@
# @medusajs/analytics-local
@@ -0,0 +1,6 @@
const defineJestConfig = require("../../../../define_jest_config")
module.exports = defineJestConfig({
moduleNameMapper: {
"^@services": "<rootDir>/src/services",
},
})
@@ -0,0 +1,42 @@
{
"name": "@medusajs/analytics-local",
"version": "2.8.2",
"description": "Local analytics provider for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/modules/providers/analytics-local"
},
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"engines": {
"node": ">=20"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"test": "jest --passWithNoTests src",
"build": "rimraf dist && tsc --build ./tsconfig.json",
"watch": "tsc --watch"
},
"devDependencies": {
"@medusajs/framework": "2.8.2",
"@swc/core": "^1.7.28",
"@swc/jest": "^0.2.36",
"jest": "^29.7.0",
"rimraf": "^5.0.1",
"typescript": "^5.6.2"
},
"peerDependencies": {
"@medusajs/framework": "2.8.2"
},
"keywords": [
"medusa-plugin",
"medusa-plugin-analytics"
]
}
@@ -0,0 +1,8 @@
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { LocalAnalyticsService } from "./services/local-analytics"
const services = [LocalAnalyticsService]
export default ModuleProvider(Modules.ANALYTICS, {
services,
})
@@ -0,0 +1,44 @@
import {
LocalAnalyticsServiceOptions,
Logger,
ProviderIdentifyAnalyticsEventDTO,
ProviderTrackAnalyticsEventDTO,
} from "@medusajs/framework/types"
import { AbstractAnalyticsProviderService } from "@medusajs/framework/utils"
type InjectedDependencies = {
logger: Logger
}
export class LocalAnalyticsService extends AbstractAnalyticsProviderService {
static identifier = "analytics-local"
protected config_: LocalAnalyticsServiceOptions
protected logger_: Logger
constructor(
{ logger }: InjectedDependencies,
options: LocalAnalyticsServiceOptions
) {
super()
this.config_ = options
this.logger_ = logger
}
async track(data: ProviderTrackAnalyticsEventDTO): Promise<void> {
this.logger_.debug(
`Tracking event: '${data.event}', actor_id: '${
data.actor_id ?? "-"
}', group: '${data.group?.id ?? "-"}', properties: '${JSON.stringify(
data.properties
)}'`
)
}
async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise<void> {
this.logger_.debug(
`Identifying user: '${data.actor_id ?? "-"}', group: '${
"group" in data ? data.group.id : "-"
}', properties: '${JSON.stringify(data.properties)}'`
)
}
}
@@ -0,0 +1,8 @@
{
"extends": "../../../../_tsconfig.base.json",
"compilerOptions": {
"paths": {
"@services": ["./src/services"]
}
}
}
@@ -0,0 +1 @@
# @medusajs/analytics-posthog
@@ -0,0 +1,6 @@
const defineJestConfig = require("../../../../define_jest_config")
module.exports = defineJestConfig({
moduleNameMapper: {
"^@services": "<rootDir>/src/services",
},
})
@@ -0,0 +1,43 @@
{
"name": "@medusajs/analytics-posthog",
"version": "2.8.2",
"description": "Posthog analytics provider for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/modules/providers/analytics-posthog"
},
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"engines": {
"node": ">=20"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"test": "jest --passWithNoTests src",
"build": "rimraf dist && tsc --build ./tsconfig.json",
"watch": "tsc --watch"
},
"devDependencies": {
"@medusajs/framework": "2.8.2",
"@swc/core": "^1.7.28",
"@swc/jest": "^0.2.36",
"jest": "^29.7.0",
"posthog-node": "^4.17.1",
"rimraf": "^5.0.1",
"typescript": "^5.6.2"
},
"peerDependencies": {
"@medusajs/framework": "2.8.2"
},
"keywords": [
"medusa-plugin",
"medusa-plugin-analytics"
]
}
@@ -0,0 +1,8 @@
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { PosthogAnalyticsService } from "./services/posthog-analytics"
const services = [PosthogAnalyticsService]
export default ModuleProvider(Modules.ANALYTICS, {
services,
})
@@ -0,0 +1,89 @@
import {
PosthogAnalyticsServiceOptions,
Logger,
ProviderIdentifyAnalyticsEventDTO,
ProviderTrackAnalyticsEventDTO,
} from "@medusajs/framework/types"
import { PostHog } from "posthog-node"
import { AbstractAnalyticsProviderService } from "@medusajs/framework/utils"
type InjectedDependencies = {
logger: Logger
}
export class PosthogAnalyticsService extends AbstractAnalyticsProviderService {
static identifier = "analytics-posthog"
protected config_: PosthogAnalyticsServiceOptions
protected logger_: Logger
protected client_: PostHog
constructor(
{ logger }: InjectedDependencies,
options: PosthogAnalyticsServiceOptions
) {
super()
this.config_ = options
this.logger_ = logger
if (!options.posthogEventsKey) {
throw new Error("Posthog API key is not set, but is required")
}
this.client_ = new PostHog(options.posthogEventsKey, {
host: options.posthogHost || "https://eu.i.posthog.com",
})
}
async track(data: ProviderTrackAnalyticsEventDTO): Promise<void> {
if (!data.event) {
throw new Error(
"Event name is required when tracking an event with Posthog"
)
}
if (!data.actor_id) {
throw new Error(
"Actor ID is required when tracking an event with Posthog"
)
}
if (data.group?.id && !data.group?.type) {
throw new Error(
"Group type is required if passing group id when tracking an event with Posthog"
)
}
this.client_.capture({
event: data.event,
distinctId: data.actor_id,
properties: data.properties,
groups: data.group?.id
? { [data.group.type!]: data.group.id }
: undefined,
})
}
async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise<void> {
if ("group" in data) {
this.client_.groupIdentify({
groupKey: data.group.id!,
groupType: data.group.type!,
properties: data.properties,
distinctId: data.actor_id,
})
} else if (data.actor_id) {
this.client_.identify({
distinctId: data.actor_id,
properties: data.properties,
})
}
throw new Error(
"Actor or group is required when identifying an entity with Posthog"
)
}
async shutdown() {
await this.client_.shutdown()
}
}
@@ -0,0 +1,8 @@
{
"extends": "../../../../_tsconfig.base.json",
"compilerOptions": {
"paths": {
"@services": ["./src/services"]
}
}
}