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
+8
View File
@@ -0,0 +1,8 @@
import { Module, Modules } from "@medusajs/framework/utils"
import AnalyticsService from "./services/analytics-service"
import loadProviders from "./loaders/providers"
export default Module(Modules.ANALYTICS, {
service: AnalyticsService,
loaders: [loadProviders],
})
@@ -0,0 +1,45 @@
import { moduleProviderLoader } from "@medusajs/framework/modules-sdk"
import {
LoaderOptions,
ModuleProvider,
ModulesSdkTypes,
} from "@medusajs/framework/types"
import { asFunction, asValue, Lifetime } from "awilix"
import ProviderService, {
AnalyticsProviderIdentifierRegistrationName,
AnalyticsProviderRegistrationPrefix,
} from "../services/provider-service"
const registrationFn = async (klass, container, pluginOptions) => {
const key = ProviderService.getRegistrationIdentifier(klass, pluginOptions.id)
container.register({
[AnalyticsProviderRegistrationPrefix + key]: asFunction(
(cradle) => new klass(cradle, pluginOptions.options ?? {}),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}
),
})
container.registerAdd(
AnalyticsProviderIdentifierRegistrationName,
asValue(key)
)
}
export default async ({
container,
options,
}: LoaderOptions<
(
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & { providers: ModuleProvider[] }
>): Promise<void> => {
await moduleProviderLoader({
container,
providers: options?.providers || [],
registerServiceFn: registrationFn,
})
}
@@ -0,0 +1,52 @@
import {
TrackAnalyticsEventDTO,
IdentifyAnalyticsEventDTO,
} from "@medusajs/types"
import AnalyticsProviderService from "./provider-service"
import { MedusaError } from "../../../../core/utils/dist/common"
type InjectedDependencies = {
analyticsProviderService: AnalyticsProviderService
}
export default class AnalyticsService {
protected readonly analyticsProviderService_: AnalyticsProviderService
constructor({ analyticsProviderService }: InjectedDependencies) {
this.analyticsProviderService_ = analyticsProviderService
}
__hooks = {
onApplicationShutdown: async () => {
await this.analyticsProviderService_.shutdown()
},
}
getProvider() {
return this.analyticsProviderService_
}
async track(data: TrackAnalyticsEventDTO): Promise<void> {
try {
await this.analyticsProviderService_.track(data)
} catch (error) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Error tracking event for ${data.event}: ${error.message}`
)
}
}
async identify(data: IdentifyAnalyticsEventDTO): Promise<void> {
try {
await this.analyticsProviderService_.identify(data)
} catch (error) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Error identifying event for ${
"group" in data ? data.group.id : data.actor_id
}: ${error.message}`
)
}
}
}
@@ -0,0 +1,56 @@
import { MedusaError } from "@medusajs/framework/utils"
import {
Constructor,
IAnalyticsProvider,
ProviderIdentifyAnalyticsEventDTO,
ProviderTrackAnalyticsEventDTO,
} from "@medusajs/types"
export const AnalyticsProviderIdentifierRegistrationName =
"analytics_providers_identifier"
export const AnalyticsProviderRegistrationPrefix = "aly_"
type InjectedDependencies = {
[
key: `${typeof AnalyticsProviderRegistrationPrefix}${string}`
]: IAnalyticsProvider
}
export default class AnalyticsProviderService {
protected readonly analyticsProvider_: IAnalyticsProvider
constructor(container: InjectedDependencies) {
const analyticsProviderKeys = Object.keys(container).filter((k) =>
k.startsWith(AnalyticsProviderRegistrationPrefix)
)
if (analyticsProviderKeys.length !== 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Analytics module should be initialized with exactly one provider`
)
}
this.analyticsProvider_ = container[analyticsProviderKeys[0]]
}
static getRegistrationIdentifier(
providerClass: Constructor<IAnalyticsProvider>,
optionName?: string
) {
return `${(providerClass as any).identifier}_${optionName}`
}
async track(data: ProviderTrackAnalyticsEventDTO): Promise<void> {
this.analyticsProvider_.track(data)
}
async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise<void> {
this.analyticsProvider_.identify(data)
}
async shutdown(): Promise<void> {
await this.analyticsProvider_.shutdown?.()
}
}
@@ -0,0 +1,24 @@
import {
ModuleProviderExports,
ModuleServiceInitializeOptions,
} from "@medusajs/framework/types"
export type AnalyticsModuleOptions = Partial<ModuleServiceInitializeOptions> & {
/**
* Providers to be registered
*/
provider?: {
/**
* The module provider to be registered
*/
resolve: string | ModuleProviderExports
/**
* The id of the provider
*/
id: string
/**
* key value pair of the configuration to be passed to the provider constructor
*/
options?: Record<string, unknown>
}
}