[feat] Integrate PostHog plugin (#8911)

* feat(plugin-posthog): initial setup of PostHog plugin

* feat(posthog): add interfaces for PostHog module configuration

* feat(posthog): add constant for injection token

* feat(posthog): implement PosthogService with track, identify and shutdown methods

* feat(posthog): implement core module with support for sync and async config

* feat(posthog): create main module with forRoot and forRootAsync

* chore(posthog):install latest posthog-node

* feat(posthog): update PostHog interfaces to match official client options

* feat(posthog): enhance PostHog service with feature flag support and alignment with official client

* feat(posthog): update PostHog interfaces to match official client options

* feat(middleware): add PosthogRequestMiddleware for capturing request context

* feat(middleware): enhance PosthogTraceMiddleware with performance metrics

* feat(interceptor): implement PosthogErrorInterceptor for error tracking

* feat(plugin): create base PosthogPlugin structure

* feat(interceptor): implement PosthogCustomInterceptor

* feat(config):Add config posthog

* add plugin

* feat(posthog): Refactor service logic && update config

* Remove unnecessary changes

* feat(posthog):Refactor and reorganize the plugin codebase for better structure and maintainability

* fix: deepscan

* feadback integration

* suggestion integration

* remove unecessary changes
This commit is contained in:
samuel mbabhazi
2025-04-20 09:52:22 +02:00
committed by GitHub
parent 69444bc0e3
commit 292e6eaa6d
53 changed files with 1964 additions and 14 deletions
@@ -33,6 +33,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: '${SENTRY_HTTP_TRACING_ENABLED:-}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${SENTRY_POSTGRES_TRACKING_ENABLED:-}'
SENTRY_PROFILING_ENABLED: '${SENTRY_PROFILING_ENABLED:-}'
POSTHOG_KEY: '${POSTHOG_KEY:-}'
POSTHOG_HOST: '${POSTHOG_HOST:-}'
POSTHOG_ENABLED: '${POSTHOG_ENABLED:-}'
POSTHOG_FLUSH_INTERVAL: '${POSTHOG_FLUSH_INTERVAL:-}'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID:-}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY:-}'
AWS_REGION: '${AWS_REGION:-}'
@@ -33,6 +33,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: '${SENTRY_HTTP_TRACING_ENABLED}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${SENTRY_POSTGRES_TRACKING_ENABLED}'
SENTRY_PROFILING_ENABLED: '${SENTRY_PROFILING_ENABLED}'
POSTHOG_KEY: '${POSTHOG_KEY}'
POSTHOG_HOST: '${POSTHOG_HOST}'
POSTHOG_ENABLED: '${POSTHOG_ENABLED}'
POSTHOG_FLUSH_INTERVAL: '${POSTHOG_FLUSH_INTERVAL}'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}'
AWS_REGION: '${AWS_REGION}'
@@ -33,6 +33,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: '${SENTRY_HTTP_TRACING_ENABLED}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${SENTRY_POSTGRES_TRACKING_ENABLED}'
SENTRY_PROFILING_ENABLED: '${SENTRY_PROFILING_ENABLED}'
POSTHOG_KEY: '${POSTHOG_KEY}'
POSTHOG_HOST: '${POSTHOG_HOST}'
POSTHOG_ENABLED: '${POSTHOG_ENABLED}'
POSTHOG_FLUSH_INTERVAL: '${POSTHOG_FLUSH_INTERVAL}'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}'
AWS_REGION: '${AWS_REGION}'
@@ -33,6 +33,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: '${SENTRY_HTTP_TRACING_ENABLED:-}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${SENTRY_POSTGRES_TRACKING_ENABLED:-}'
SENTRY_PROFILING_ENABLED: '${SENTRY_PROFILING_ENABLED:-}'
POSTHOG_KEY: '${POSTHOG_KEY:-}'
POSTHOG_HOST: '${POSTHOG_HOST:-}'
POSTHOG_ENABLED: '${POSTHOG_ENABLED:-}'
POSTHOG_FLUSH_INTERVAL: '${POSTHOG_FLUSH_INTERVAL:-}'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID:-}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY:-}'
AWS_REGION: '${AWS_REGION:-}'
@@ -33,6 +33,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: '${SENTRY_HTTP_TRACING_ENABLED}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${SENTRY_POSTGRES_TRACKING_ENABLED}'
SENTRY_PROFILING_ENABLED: '${SENTRY_PROFILING_ENABLED}'
POSTHOG_KEY: '${POSTHOG_KEY}'
POSTHOG_HOST: '${POSTHOG_HOST}'
POSTHOG_ENABLED: '${POSTHOG_ENABLED}'
POSTHOG_FLUSH_INTERVAL: '${POSTHOG_FLUSH_INTERVAL}'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}'
AWS_REGION: '${AWS_REGION}'
@@ -33,6 +33,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: '${SENTRY_HTTP_TRACING_ENABLED}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${SENTRY_POSTGRES_TRACKING_ENABLED}'
SENTRY_PROFILING_ENABLED: '${SENTRY_PROFILING_ENABLED}'
POSTHOG_KEY: '${POSTHOG_KEY}'
POSTHOG_HOST: '${POSTHOG_HOST}'
POSTHOG_ENABLED: '${POSTHOG_ENABLED}'
POSTHOG_FLUSH_INTERVAL: '${POSTHOG_FLUSH_INTERVAL}'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}'
AWS_REGION: '${AWS_REGION}'
+6
View File
@@ -250,6 +250,12 @@ SENTRY_POSTGRES_TRACKING_ENABLED=false
SENTRY_PROFILING_ENABLED=false
SENTRY_TRACES_SAMPLE_RATE=0.1
# PostHog Configuration
POSTHOG_KEY=
POSTHOG_HOST=https://app.posthog.com
POSTHOG_ENABLED=true
POSTHOG_FLUSH_INTERVAL=10000
# Default Currency
DEFAULT_CURRENCY=USD
+8
View File
@@ -257,6 +257,14 @@ SENTRY_POSTGRES_TRACKING_ENABLED=false
SENTRY_PROFILING_ENABLED=false
SENTRY_TRACES_SAMPLE_RATE=0.1
# PostHog Configuration
POSTHOG_KEY=
POSTHOG_HOST=https://app.posthog.com
POSTHOG_ENABLED=true
POSTHOG_FLUSH_INTERVAL=10000
# Default Currency
DEFAULT_CURRENCY=USD
+6
View File
@@ -237,6 +237,12 @@ SENTRY_POSTGRES_TRACKING_ENABLED=false
SENTRY_PROFILING_ENABLED=false
SENTRY_TRACES_SAMPLE_RATE=0.1
# PostHog Configuration
POSTHOG_KEY=
POSTHOG_HOST=https://app.posthog.com
POSTHOG_ENABLED=true
POSTHOG_FLUSH_INTERVAL=10000
# Default Currency
DEFAULT_CURRENCY=USD
+6
View File
@@ -242,6 +242,12 @@ SENTRY_PROFILING_ENABLED=false
SENTRY_TRACES_SAMPLE_RATE=0.01
SENTRY_PROFILE_SAMPLE_RATE=0.01
# PostHog Configuration
POSTHOG_KEY=
POSTHOG_HOST=https://us.i.posthog.com
POSTHOG_ENABLED=false
POSTHOG_FLUSH_INTERVAL=10000
# Default Currency
DEFAULT_CURRENCY=USD
+6
View File
@@ -241,6 +241,12 @@ SENTRY_PROFILING_ENABLED=
SENTRY_TRACES_SAMPLE_RATE=
SENTRY_PROFILE_SAMPLE_RATE=
# PostHog Configuration
POSTHOG_KEY=
POSTHOG_HOST=
POSTHOG_ENABLED=
POSTHOG_FLUSH_INTERVAL=
# Default Currency
DEFAULT_CURRENCY=USD
+1
View File
@@ -55,6 +55,7 @@
"@gauzy/plugin-knowledge-base": "^0.1.0",
"@gauzy/plugin-product-reviews": "^0.1.0",
"@gauzy/plugin-sentry": "^0.1.0",
"@gauzy/plugin-posthog": "^0.1.0",
"@gauzy/plugin-videos": "^0.1.0",
"@gauzy/plugin-registry": "^0.1.0",
"dotenv": "^16.0.3",
+28 -2
View File
@@ -9,11 +9,13 @@ import {
dbKnexConnectionConfig
} from '@gauzy/config';
import { SentryService } from '@gauzy/plugin-sentry';
import { PosthogService } from '@gauzy/plugin-posthog';
import { PosthogAnalytics as PosthogPlugin } from './posthog';
import { SentryTracing as SentryPlugin } from './sentry';
import { version } from './../version';
import { plugins } from './plugins';
const { sentry } = environment;
const { sentry, posthog } = environment;
console.log(chalk.magenta(`API Version %s`), version);
console.log('Plugin Config -> __dirname: ' + __dirname);
@@ -90,6 +92,30 @@ export const pluginConfig: ApplicationPluginConfig = {
assetPath: assetPath,
assetPublicPath: assetPublicPath
},
...(sentry?.dsn ? { logger: new SentryService(SentryPlugin.options) } : {}),
logger: (() => {
const loggers = [];
if (sentry?.dsn) {
loggers.push(new SentryService(SentryPlugin.options));
}
if (posthog?.posthogEnabled && posthog?.posthogKey) {
loggers.push(new PosthogService(PosthogPlugin.options));
}
// Combine both, or return undefined if no logger is configured
if (loggers.length === 0) {
return undefined;
} else if (loggers.length === 1) {
return loggers[0];
} else {
return {
log: (...args) => loggers.forEach((logger) => logger.log?.(...args)),
error: (...args) => loggers.forEach((logger) => logger.error?.(...args)),
warn: (...args) => loggers.forEach((logger) => logger.warn?.(...args)),
debug: (...args) => loggers.forEach((logger) => logger.debug?.(...args)),
verbose: (...args) => loggers.forEach((logger) => logger.verbose?.(...args))
};
}
})(),
plugins
};
+6 -1
View File
@@ -17,8 +17,9 @@ import { VideosPlugin } from '@gauzy/plugin-videos';
import { RegistryPlugin } from '@gauzy/plugin-registry';
import { SentryTracing as SentryPlugin } from './sentry';
import { PosthogAnalytics as PosthogPlugin } from './posthog';
const { jitsu, sentry } = environment;
const { jitsu, sentry, posthog } = environment;
/**
* An array of plugins to be included or used in the codebase.
@@ -26,6 +27,10 @@ const { jitsu, sentry } = environment;
export const plugins = [
// Includes the SentryPlugin based on the presence of Sentry configuration.
...(sentry && sentry.dsn ? [SentryPlugin] : []),
// Includes the PostHogPlugin based on the presence of PostHog configuration.
...(posthog?.posthogEnabled && posthog?.posthogKey ? [PosthogPlugin] : []),
// Initializes the Jitsu Analytics Plugin by providing a configuration object.
JitsuAnalyticsPlugin.init({
config: {
+31
View File
@@ -0,0 +1,31 @@
import { environment } from '@gauzy/config';
import { PosthogPlugin } from '@gauzy/plugin-posthog';
/**
* Initializes and configures the PostHog plugin for analytics and performance tracking.
*
* This function checks if PostHog tracking is enabled in the environment configuration.
* If enabled, it initializes the PostHog plugin with the specified settings.
* Otherwise, it logs a message indicating that PostHog was not initialized.
*
* @returns {typeof PosthogPlugin | null} The configured PostHog instance, or null if not initialized.
*/
export function initializePosthog(): typeof PosthogPlugin | null {
if (!environment.posthog?.posthogEnabled || !environment.posthog?.posthogKey) {
console.log('PostHog not initialized: Tracking is disabled or API key is missing');
return null;
}
// Configure PostHog
return PosthogPlugin.init({
apiKey: environment.posthog.posthogKey,
apiHost: environment.posthog.posthogHost || 'https://app.posthog.com',
enableErrorTracking: true,
flushInterval: environment.posthog.posthogFlushInterval || 10000,
flushAt: 20,
autocapture: true,
mock: false
});
}
export const PosthogAnalytics = initializePosthog();
+4
View File
@@ -24,6 +24,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-}
SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-}
SENTRY_PROFILING_ENABLED: ${SENTRY_PROFILING_ENABLED:-}
POSTHOG_KEY: ${POSTHOG_KEY:-}
POSTHOG_HOST: ${POSTHOG_HOST:-}
POSTHOG_ENABLED: ${POSTHOG_ENABLED:-}
POSTHOG_FLUSH_INTERVAL: ${POSTHOG_FLUSH_INTERVAL:-}
JITSU_SERVER_URL: ${JITSU_SERVER_URL:-}
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-}
+4
View File
@@ -35,6 +35,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-}
SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-}
SENTRY_PROFILING_ENABLED: ${SENTRY_PROFILING_ENABLED:-}
POSTHOG_KEY: ${POSTHOG_KEY:-}
POSTHOG_HOST: ${POSTHOG_HOST:-}
POSTHOG_ENABLED: ${POSTHOG_ENABLED:-}
POSTHOG_FLUSH_INTERVAL: ${POSTHOG_FLUSH_INTERVAL:-}
JITSU_SERVER_URL: ${JITSU_SERVER_URL:-}
JITSU_SERVER_WRITE_KEY: ${JITSU_SERVER_WRITE_KEY:-}
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
+4
View File
@@ -17,6 +17,10 @@ services:
SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-}
SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-}
SENTRY_PROFILING_ENABLED: ${SENTRY_PROFILING_ENABLED:-}
POSTHOG_KEY: ${POSTHOG_KEY:-}
POSTHOG_HOST: ${POSTHOG_HOST:-}
POSTHOG_ENABLED: ${POSTHOG_ENABLED:-}
POSTHOG_FLUSH_INTERVAL: ${POSTHOG_FLUSH_INTERVAL:-}
JITSU_SERVER_URL: ${JITSU_SERVER_URL:-}
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-}
+6 -3
View File
@@ -170,9 +170,9 @@
"build:package:plugins:pre": "yarn run build:package:ui-core && yarn run build:package:ui-auth && yarn run build:package:plugin:onboarding-ui && yarn run build:package:plugin:legal-ui && yarn run build:package:plugin:job-search-ui && yarn run build:package:plugin:job-matching-ui && yarn run build:package:plugin:job-employee-ui && yarn run build:package:plugin:job-proposal-ui && yarn run build:package:plugin:public-layout-ui && yarn run build:package:plugin:maintenance-ui && yarn run build:package:plugin:videos-ui && yarn run build:integration-ui-plugins",
"build:package:plugins:pre:prod": "yarn run build:package:ui-core:prod && yarn run build:package:ui-auth:prod && yarn run build:package:plugin:onboarding-ui:prod && yarn run build:package:plugin:legal-ui:prod && yarn run build:package:plugin:job-search-ui:prod && yarn run build:package:plugin:job-matching-ui:prod && yarn run build:package:plugin:job-employee-ui:prod && yarn run build:package:plugin:job-proposal-ui:prod && yarn run build:package:plugin:public-layout-ui:prod && yarn run build:package:plugin:maintenance-ui:prod && yarn run build:package:plugin:videos-ui:prod && yarn run build:integration-ui-plugins:prod",
"build:package:plugins:pre:docker": "yarn run build:package:ui-core:docker && yarn run build:package:ui-auth:docker && yarn run build:package:plugin:onboarding-ui:docker && yarn run build:package:plugin:legal-ui:docker && yarn run build:package:plugin:job-search-ui:docker && yarn run build:package:plugin:job-matching-ui:docker && yarn run build:package:plugin:job-employee-ui:docker && yarn run build:package:plugin:job-proposal-ui:docker && yarn run build:package:plugin:public-layout-ui:docker && yarn run build:package:plugin:maintenance-ui:docker && yarn run build:package:plugin:videos-ui:docker && yarn run build:integration-ui-plugins:docker",
"build:package:plugins:post": "yarn run build:package:plugin:integration-jira && yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:integration-make-com && yarn run build:package:plugin:videos && yarn run build:package:plugin:registry && yarn run build:package:plugin:integration-zapier",
"build:package:plugins:post:prod": "yarn run build:package:plugin:integration-jira:prod && yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod && yarn run build:package:plugin:integration-make-com:prod && yarn run build:package:plugin:videos:prod && yarn run build:package:plugin:registry:prod && yarn run build:package:plugin:integration-zapier:prod",
"build:package:plugins:post:docker": "yarn run build:package:plugin:integration-jira:docker && yarn run build:package:plugin:integration-ai:docker && yarn run build:package:plugin:sentry:docker && yarn run build:package:plugin:jitsu-analytic:docker && yarn run build:package:plugin:product-reviews:docker && yarn run build:package:plugin:job-search:docker && yarn run build:package:plugin:job-proposal:docker && yarn run build:package:plugin:integration-github:docker && yarn run build:package:plugin:knowledge-base:docker && yarn run build:package:plugin:changelog:docker && yarn run build:package:plugin:integration-hubstaff:docker && yarn run build:package:plugin:integration-upwork:docker && yarn run build:package:plugin:integration-make-com:docker && yarn run build:package:plugin:videos:docker && yarn run build:package:plugin:registry:docker && yarn run build:package:plugin:integration-zapier:docker",
"build:package:plugins:post": "yarn run build:package:plugin:integration-jira && yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:sentry && yarn run build:package:plugin:posthog && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:integration-make-com && yarn run build:package:plugin:videos && yarn run build:package:plugin:registry && yarn run build:package:plugin:integration-zapier",
"build:package:plugins:post:prod": "yarn run build:package:plugin:integration-jira:prod && yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:posthog:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod && yarn run build:package:plugin:integration-make-com:prod && yarn run build:package:plugin:videos:prod && yarn run build:package:plugin:registry:prod && yarn run build:package:plugin:integration-zapier:prod",
"build:package:plugins:post:docker": "yarn run build:package:plugin:integration-jira:docker && yarn run build:package:plugin:integration-ai:docker && yarn run build:package:plugin:sentry:docker && yarn run build:package:plugin:posthog:docker && yarn run build:package:plugin:jitsu-analytic:docker && yarn run build:package:plugin:product-reviews:docker && yarn run build:package:plugin:job-search:docker && yarn run build:package:plugin:job-proposal:docker && yarn run build:package:plugin:integration-github:docker && yarn run build:package:plugin:knowledge-base:docker && yarn run build:package:plugin:changelog:docker && yarn run build:package:plugin:integration-hubstaff:docker && yarn run build:package:plugin:integration-upwork:docker && yarn run build:package:plugin:integration-make-com:docker && yarn run build:package:plugin:videos:docker && yarn run build:package:plugin:registry:docker && yarn run build:package:plugin:integration-zapier:docker",
"build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-integration-ai",
"build:package:plugin:integration-ai:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-integration-ai",
"build:package:plugin:integration-ai:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-integration-ai",
@@ -197,6 +197,9 @@
"build:package:plugin:sentry": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-sentry",
"build:package:plugin:sentry:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-sentry",
"build:package:plugin:sentry:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-sentry",
"build:package:plugin:posthog": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-posthog",
"build:package:plugin:posthog:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-posthog",
"build:package:plugin:posthog:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-posthog",
"build:package:plugin:jitsu-analytic": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-jitsu-analytics",
"build:package:plugin:jitsu-analytic:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-jitsu-analytics",
"build:package:plugin:jitsu-analytic:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-jitsu-analytics",
@@ -0,0 +1,24 @@
/**
* Represents a configuration object for PostHog server settings.
*/
export interface IPosthogConfig {
/**
* The PostHog API key used to authenticate requests.
*/
readonly posthogKey: string;
/**
* The host URL of the PostHog instance (e.g., https://app.posthog.com).
*/
readonly posthogHost: string;
/**
* Whether PostHog tracking is enabled.
*/
readonly posthogEnabled: boolean;
/**
* How often events are flushed (in milliseconds).
*/
readonly posthogFlushInterval: number;
}
+7 -7
View File
@@ -1,25 +1,25 @@
export * from './IApplicationPluginConfig';
export * from './IGraphql';
export * from './IAbstractLogger';
export * from './IAppIntegrationConfig';
export * from './IApplicationPluginConfig';
export * from './IAuth0Config';
export * from './IAwsConfig';
export * from './ICloudinaryConfig';
export * from './IDigitalOceanConfig';
export * from './IFacebookConfig';
export * from './IFiverrConfig';
export * from './IGithubConfig';
export * from './IGoogleConfig';
export * from './IGraphql';
export * from './IHubstaffConfig';
export * from './IJiraIntegrationConfig';
export * from './IJitsuConfig';
export * from './IKeycloakConfig';
export * from './ILinkedinIConfig';
export * from './IMicrosoftConfig';
export * from './IPosthogConfig';
export * from './ISMTPConfig';
export * from './ITwitterConfig';
export * from './IUnleashConfig';
export * from './IUpworkConfig';
export * from './IWasabiConfig';
export * from './IDigitalOceanConfig';
export * from './IHubstaffConfig';
export * from './IJitsuConfig';
export * from './IJiraIntegrationConfig';
export * from './IZapierConfig';
@@ -171,6 +171,13 @@ export const environment: IEnvironment = {
dsn: process.env.SENTRY_DSN
},
posthog: {
posthogKey: process.env.POSTHOG_KEY,
posthogHost: process.env.POSTHOG_HOST,
posthogEnabled: process.env.POSTHOG_ENABLED === 'true',
posthogFlushInterval: parseInt(process.env.POSTHOG_FLUSH_INTERVAL) || 10000
},
defaultIntegratedUserPass: process.env.INTEGRATED_USER_DEFAULT_PASS || '123456',
upwork: {
@@ -171,6 +171,13 @@ export const environment: IEnvironment = {
dsn: process.env.SENTRY_DSN
},
posthog: {
posthogKey: process.env.POSTHOG_KEY,
posthogHost: process.env.POSTHOG_HOST,
posthogEnabled: process.env.POSTHOG_ENABLED === 'true',
posthogFlushInterval: parseInt(process.env.POSTHOG_FLUSH_INTERVAL) || 10000
},
defaultIntegratedUserPass: process.env.INTEGRATED_USER_DEFAULT_PASS || '123456',
upwork: {
@@ -17,7 +17,8 @@ import {
IWasabiConfig,
IJiraIntegrationConfig,
IDigitalOceanConfig,
IZapierConfig
IZapierConfig,
IPosthogConfig
} from '@gauzy/common';
import { FileStorageProviderEnum } from '@gauzy/contracts';
@@ -109,6 +110,8 @@ export interface IEnvironment {
dsn: string;
};
posthog?: IPosthogConfig;
/**
* Default Integrated User Password
*/
+23
View File
@@ -0,0 +1,23 @@
# Ignore Git related files and directories
.git
.gitignore
.gitmodules
# Ignore README file
README.md
# Ignore Docker-related files and directories
docker
# Ignore Node.js modules
node_modules
# Ignore temporary files and directories
tmp
# Ignore build and compilation output
build
dist
# Ignore environment configuration files
.env
+11
View File
@@ -0,0 +1,11 @@
# dependencies
node_modules/
# yarn
yarn-error.log
# misc
npm-debug.log
dist
build
+4
View File
@@ -0,0 +1,4 @@
# .npmignore
src/
node_modules/
+3
View File
@@ -0,0 +1,3 @@
# Changelog for @gauzy/plugin-posthog
## [Unreleased]
+42
View File
@@ -0,0 +1,42 @@
# @gauzy/plugin-posthog
## Overview
The PostHog Plugin seamlessly integrates your server with [PostHog](https://posthog.com), an open-source analytics platform. This integration enables you to capture, monitor, and analyze user behavior and application events in real time.
## Features
- **Error Tracking**: Automatically report exceptions and errors to PostHog’s new error tracking feature.
- **User Behavior Insights**: Gain comprehensive insights into user interactions within your application.
- **Event Tracking**: Capture custom application events for detailed analysis.
## Configuration
The plugin can be configured using the following environment variables:
- `POSTHOG_KEY`: Your PostHog API key
- `POSTHOG_HOST`: PostHog server URL (default: https://app.posthog.com)
- `POSTHOG_ENABLED`: Enable/disable the plugin (default: false)
- `POSTHOG_FLUSH_INTERVAL`: Interval for flushing events in milliseconds (default: 10000)
## Building
Run `yarn nx build plugin-posthog` to build the library.
## Running unit tests
Run `yarn nx test plugin-posthog` to execute the unit tests via [Jest](https://jestjs.io).
## Publishing
After building your library with `yarn nx build plugin-posthog`, go to the dist folder `dist/packages/plugins/posthog` and run `npm publish`.
## Installation
Install the PostHog Plugin using your preferred package manager:
```bash
npm install @gauzy/plugin-posthog
# or
yarn add @gauzy/plugin-posthog
```
+19
View File
@@ -0,0 +1,19 @@
const baseConfig = require('../../../.eslintrc.json');
module.exports = [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}']
}
]
},
languageOptions: {
parser: require('jsonc-eslint-parser')
}
}
];
+10
View File
@@ -0,0 +1,10 @@
export default {
displayName: 'posthog',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/packages/plugins/posthog'
};
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@gauzy/plugin-posthog",
"version": "0.1.0",
"description": "Ever Gauzy Platform PostHog Plugin",
"author": {
"name": "Ever Co. LTD",
"email": "ever@ever.co",
"url": "https://ever.co"
},
"license": "AGPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/ever-co/ever-gauzy",
"directory": "packages/plugins/posthog"
},
"bugs": {
"url": "https://github.com/ever-co/ever-gauzy/issues"
},
"homepage": "https://ever.co",
"private": true,
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"scripts": {
"lib:build": "yarn nx build plugin-posthog",
"lib:build:prod": "yarn nx build plugin-posthog",
"lib:watch": "yarn nx build plugin-posthog --watch"
},
"dependencies": {
"@gauzy/config": "^0.1.0",
"@gauzy/core": "^0.1.0",
"@gauzy/plugin": "^0.1.0",
"@nestjs/common": "^10.4.15",
"chalk": "^4.1.0",
"posthog-node": "^4.11.3",
"tslib": "^2.6.2"
},
"devDependencies": {
"@types/jest": "29.5.14",
"@types/node": "^20.14.9",
"typescript": "5.5.4"
},
"keywords": [
"gauzy",
"plugin",
"posthog",
"ever",
"platform",
"error-tracking"
],
"engines": {
"node": ">=20.11.1",
"yarn": ">=1.22.19"
},
"sideEffects": false
}
+49
View File
@@ -0,0 +1,49 @@
{
"name": "plugin-posthog",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/plugins/posthog/src",
"projectType": "library",
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag"
}
}
},
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/plugins/posthog",
"tsConfig": "packages/plugins/posthog/tsconfig.lib.json",
"packageJson": "packages/plugins/posthog/package.json",
"main": "packages/plugins/posthog/src/index.ts",
"assets": [
"packages/plugins/posthog/*.md"
]
}
},
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"options": {
"jestConfig": "packages/plugins/posthog/jest.config.ts"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
/**
* Public API Surface of @gauzy/plugin-posthog
*/
export * from './lib/posthog.plugin';
export * from './lib/posthog.service';
@@ -0,0 +1,87 @@
import { DynamicModule, Global, Module, Provider, Type } from '@nestjs/common';
import { PosthogModuleOptions, PosthogModuleAsyncOptions, PosthogOptionsFactory } from './posthog.interfaces';
import { POSTHOG_MODULE_OPTIONS, POSTHOG_TOKEN } from './posthog.constants';
import { PosthogService } from './posthog.service';
import { createPosthogProviders } from './posthog.providers';
@Global()
@Module({})
export class PosthogCoreModule {
/**
* Synchronous registration of the Posthog module
* @param options - Configuration object for Posthog
* @returns A dynamic module with providers and exports
*/
static forRoot(options: PosthogModuleOptions): DynamicModule {
const provider = createPosthogProviders(options);
return {
module: PosthogCoreModule,
providers: [provider, PosthogService],
exports: [provider, PosthogService]
};
}
/**
* Asynchronous registration of the Posthog module
* Supports useFactory, useClass, or useExisting strategies
* @param options - Async module options including factory or class
* @returns A dynamic module with async providers and exports
*/
static forRootAsync(options: PosthogModuleAsyncOptions): DynamicModule {
const provider: Provider = {
provide: POSTHOG_TOKEN,
useFactory: (options: PosthogModuleOptions) => new PosthogService(options),
inject: [POSTHOG_MODULE_OPTIONS]
};
return {
module: PosthogCoreModule,
imports: options.imports || [],
providers: [...this.createAsyncProviders(options), provider, PosthogService],
exports: [provider, PosthogService]
};
}
/**
* Creates async providers based on the chosen async strategy
* @param options - Configuration for async provider setup
* @returns An array of providers to be used in the async module
*/
private static createAsyncProviders(options: PosthogModuleAsyncOptions): Provider[] {
// If a factory function is provided directly
if (options.useFactory) {
return [
{
provide: POSTHOG_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || []
}
];
}
// Dependency injection array for useClass or useExisting
if (!options.useClass && !options.useExisting) {
throw new Error('Either useClass or useExisting must be provided for PosthogModule async configuration');
}
const inject = [(options.useClass || options.useExisting) as Type<PosthogOptionsFactory>];
const providers: Provider[] = [
{
provide: POSTHOG_MODULE_OPTIONS,
useFactory: async (factory: PosthogOptionsFactory) => await factory.createPosthogOptions(),
inject
}
];
// Register useClass as a provider if defined
if (options.useClass) {
providers.push({
provide: options.useClass,
useClass: options.useClass
});
}
return providers;
}
}
@@ -0,0 +1,534 @@
// Nestjs imports
import {
CallHandler,
ExecutionContext,
HttpException,
Inject,
Injectable,
NestInterceptor,
Optional
} from '@nestjs/common';
import { ContextType, HttpArgumentsHost, RpcArgumentsHost, WsArgumentsHost } from '@nestjs/common/interfaces';
// Rxjs imports
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PosthogInterceptorOptions, PosthogInterceptorOptionsFilter } from './posthog.interfaces';
import { PosthogService } from './posthog.service';
import { SanitizerUtil } from './utils';
import { POSTHOG_MODULE_OPTIONS } from './posthog.constants';
/**
* Interceptor for capturing and tracking exceptions through PostHog analytics.
* Sanitizes sensitive data before sending to PostHog and provides comprehensive error context.
*/
@Injectable()
export class PosthogErrorInterceptor implements NestInterceptor {
/**
* PostHog service instance for tracking events
*/
protected readonly client: PosthogService = PosthogService.PosthogServiceInstance();
private readonly options?: PosthogInterceptorOptions;
/**
* @param options - Configuration options for the interceptor
*/
constructor(@Optional() @Inject(POSTHOG_MODULE_OPTIONS) options?: PosthogInterceptorOptions) {
this.options = options;
}
/**
* Intercepts requests and captures exceptions when they occur
*
* @param context - Execution context of the current request
* @param next - Call handler for the request pipeline
* @returns Observable of the response stream
*/
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const requestStartTime = Date.now();
return next.handle().pipe(
tap(null, (exception: any) => {
// Calculate error timing metrics
const errorTiming = {
timestamp: new Date().toISOString(),
duration_ms: Date.now() - requestStartTime
};
// Report to PostHog if it passes the filter
if (this.shouldReport(exception)) {
this.captureException(context, exception, errorTiming);
}
})
);
}
/**
* Routes exception capture based on context type (HTTP, RPC, WebSocket)
*
* @param context - Execution context of the current request
* @param exception - The thrown exception to be captured
* @param errorTiming - Timing information about when the error occurred
*/
protected captureException(
context: ExecutionContext,
exception: any,
errorTiming: { timestamp: string; duration_ms: number }
) {
switch (context.getType<ContextType>()) {
case 'http':
return this.captureHttpException(context.switchToHttp(), exception, errorTiming);
case 'rpc':
return this.captureRpcException(context.switchToRpc(), exception, errorTiming);
case 'ws':
return this.captureWsException(context.switchToWs(), exception, errorTiming);
default:
// Fallback for unknown context types
this.captureGenericException(context, exception, errorTiming);
}
}
/**
* Captures HTTP exceptions with comprehensive request context
*
* @param http - HTTP arguments host containing request data
* @param exception - The thrown HTTP exception
* @param errorTiming - Timing information about when the error occurred
*/
private captureHttpException(
http: HttpArgumentsHost,
exception: HttpException,
errorTiming: { timestamp: string; duration_ms: number }
): void {
const request = http.getRequest();
const response = http.getResponse();
// Extract user identifier with fallbacks
const userId = this.extractUserIdentifier(request);
// Extract status code with error-aware fallback
const statusCode = this.getStatusCode(exception);
// Extract route information
const route = this.extractRouteInfo(request);
// Create a data object with request details
const data = {
request: {
method: request.method,
url: request.originalUrl || request.url,
path: request.path,
route: route,
headers: SanitizerUtil.sanitizeHeaders(request.headers),
query: SanitizerUtil.sanitizeObject(request.query),
body: SanitizerUtil.sanitizeObject(request.body),
params: SanitizerUtil.sanitizeObject(request.params)
},
user: {
ip: request.ip,
id: request.user?.id || request.user?.userId,
email: request.user?.email,
roles: request.user?.roles,
agent: request.headers['user-agent']
},
response: {
status_code: statusCode,
headers: response?.getHeaders ? SanitizerUtil.sanitizeHeaders(response.getHeaders()) : undefined
},
error: {
message: exception.message,
name: exception.name,
stack: this.sanitizeStack(exception.stack),
status: statusCode,
response: SanitizerUtil.sanitizeObject(exception.getResponse?.() || null),
cause: exception.cause ? String(exception.cause) : undefined
},
context: {
handler: this.getHandlerName(http.getRequest()?.__routeInfo),
controller: this.getControllerName(http.getRequest()?.__routeInfo)
},
environment: {
nodeEnv: process.env['NODE_ENV'],
appVersion: process.env['APP_VERSION'],
serviceName: process.env['SERVICE_NAME'] || 'api',
timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
}
};
// Track the error with improved data structure
this.client.captureException(exception, userId, {
error_message: exception.message,
error_name: exception.name,
stack: this.sanitizeStack(exception.stack),
status_code: data.response.status_code,
request_data: data.request,
user_data: data.user,
response_data: data.response,
error_data: data.error,
context_data: data.context,
environment_data: data.environment,
$timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms,
http_method: request.method
});
// Add tags for better error categorization in PostHog
if (this.options?.includeTags !== false) {
this.addErrorTags(userId, {
error_type: exception.name,
status_code: String(statusCode),
http_method: request.method,
path: route || request.path,
environment: process.env['NODE_ENV'] || 'development'
});
}
}
/**
* Captures RPC exceptions with message context and improved data
*
* @param rpc - RPC arguments host
* @param exception - The thrown exception
* @param errorTiming - Timing information about when the error occurred
*/
private captureRpcException(
rpc: RpcArgumentsHost,
exception: any,
errorTiming: { timestamp: string; duration_ms: number }
): void {
const data = rpc.getData();
const ctx = rpc.getContext();
// Try to extract pattern information
const pattern = typeof ctx.getPattern === 'function' ? ctx.getPattern() : 'unknown-pattern';
// Try to extract client ID or fallback to system
const clientId = this.extractRpcClientId(ctx) || 'system';
// Create detailed error context
const errorContext = {
rpc_data: SanitizerUtil.sanitizeObject(data),
rpc_context: SanitizerUtil.sanitizeObject(ctx),
rpc_pattern: pattern,
error: {
message: exception.message,
name: exception.name,
stack: this.sanitizeStack(exception.stack),
cause: exception.cause ? String(exception.cause) : undefined
},
environment: {
nodeEnv: process.env['NODE_ENV'],
appVersion: process.env['APP_VERSION'],
serviceName: process.env['SERVICE_NAME'] || 'api',
timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
}
};
this.client.captureException(exception, clientId, {
error_message: exception.message,
error_name: exception.name,
stack: this.sanitizeStack(exception.stack),
rpc_data: errorContext.rpc_data,
rpc_pattern: errorContext.rpc_pattern,
rpc_context: errorContext.rpc_context,
error_data: errorContext.error,
environment_data: errorContext.environment,
$timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
});
// Add tags for better error categorization
if (this.options?.includeTags !== false) {
this.addErrorTags(clientId, {
error_type: exception.name,
pattern: pattern,
environment: process.env['NODE_ENV'] || 'development',
message_type: 'rpc'
});
}
}
/**
* Captures WebSocket exceptions with client context and improved data
*
* @param ws - WebSocket arguments host
* @param exception - The thrown exception
* @param errorTiming - Timing information about when the error occurred
*/
private captureWsException(
ws: WsArgumentsHost,
exception: any,
errorTiming: { timestamp: string; duration_ms: number }
): void {
const client = ws.getClient();
const data = ws.getData();
// Extract client ID with fallbacks
const clientId = client.id || this.extractWsClientId(client) || 'websocket-client';
// Try to extract event type
const eventType = typeof data === 'object' && data.event ? data.event : 'unknown-event';
// Create detailed error context
const errorContext = {
ws_client: {
id: client.id,
handshake: SanitizerUtil.sanitizeObject(client.handshake)
},
ws_data: SanitizerUtil.sanitizeObject(data),
ws_event: eventType,
error: {
message: exception.message,
name: exception.name,
stack: this.sanitizeStack(exception.stack),
cause: exception.cause ? String(exception.cause) : undefined
},
environment: {
nodeEnv: process.env['NODE_ENV'],
appVersion: process.env['APP_VERSION'],
serviceName: process.env['SERVICE_NAME'] || 'api',
timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
}
};
this.client.captureException(exception, clientId, {
error_message: exception.message,
error_name: exception.name,
stack: this.sanitizeStack(exception.stack),
ws_client: errorContext.ws_client,
ws_data: errorContext.ws_data,
ws_event: errorContext.ws_event,
error_data: errorContext.error,
environment_data: errorContext.environment,
$timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
});
// Add tags for better error categorization
if (this.options?.includeTags !== false) {
this.addErrorTags(clientId, {
error_type: exception.name,
event_type: eventType,
environment: process.env['NODE_ENV'] || 'development',
message_type: 'websocket'
});
}
}
/**
* Captures exceptions from unknown context types
*
* @param context - Execution context
* @param exception - The thrown exception
* @param errorTiming - Timing information about when the error occurred
*/
private captureGenericException(
context: ExecutionContext,
exception: any,
errorTiming: { timestamp: string; duration_ms: number }
): void {
// Use a generic system identifier
const contextId = 'system';
const errorContext = {
context_type: context.getType<string>(),
handler: this.getHandlerName(context),
error: {
message: exception.message,
name: exception.name,
stack: this.sanitizeStack(exception.stack),
cause: exception.cause ? String(exception.cause) : undefined
},
environment: {
nodeEnv: process.env['NODE_ENV'],
appVersion: process.env['APP_VERSION'],
serviceName: process.env['SERVICE_NAME'] || 'api',
timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
}
};
this.client.captureException(exception, contextId, {
error_message: exception.message,
error_name: exception.name,
stack: this.sanitizeStack(exception.stack),
context_data: {
type: errorContext.context_type,
handler: errorContext.handler
},
error_data: errorContext.error,
environment_data: errorContext.environment,
$timestamp: errorTiming.timestamp,
duration_ms: errorTiming.duration_ms
});
}
/**
* Adds error tags for better categorization in PostHog
*
* @param distinctId - User or client identifier
* @param tags - Tags to associate with the error
*/
private addErrorTags(distinctId: string, tags: Record<string, string>): void {
// Add error tags as a separate event for better filtering in PostHog
this.client.track('error_tags', distinctId, tags);
}
/**
* Determines if an exception should be reported based on configured filters
*
* @param exception - The thrown exception
* @returns Boolean indicating if the exception should be reported
*/
shouldReport(exception: any): boolean {
// If no options or filters are defined, always report
if (!this.options) return true;
// If any filter passes, then we do not report
const filters: PosthogInterceptorOptionsFilter[] = this.options.filters || [];
return !filters.some(({ type, filter }) => {
return exception instanceof type && (!filter || filter(exception));
});
}
/**
* Extracts a user identifier from various sources in the request
*
* @param request - The HTTP request
* @returns User identifier string
*/
private extractUserIdentifier(request: any): string {
return (
request.user?.id ||
request.user?.userId ||
request.headers['x-user-id'] ||
request.cookies?.userId ||
request.ip ||
'anonymous'
);
}
/**
* Extracts a client ID from RPC context
*
* @param ctx - RPC context
* @returns Client ID string or null
*/
private extractRpcClientId(ctx: any): string | null {
return ctx.clientId ?? ctx.args?.[0]?.user?.id ?? null;
}
/**
* Extracts a client ID from WebSocket client
*
* @param client - WebSocket client
* @returns Client ID string or null
*/
private extractWsClientId(client: any): string | null {
if (!client) return null;
// Try to extract user ID from handshake data
if (client.handshake?.query?.userId) {
return client.handshake.query.userId;
}
// Try to extract from auth data
if (client.handshake?.auth?.userId) {
return client.handshake.auth.userId;
}
return null;
}
/**
* Gets the handler name from the execution context
*
* @param context - Execution context or route info
* @returns Handler name string
*/
private getHandlerName(context: ExecutionContext | any): string {
if (!context) return 'unknown';
// If passed route info directly
if (context.handler) return context.handler;
// If passed execution context
if (typeof context.getHandler === 'function') {
const handler = context.getHandler();
return handler.name || 'unknown';
}
return 'unknown';
}
/**
* Gets the controller name from route info
*
* @param routeInfo - Route information object
* @returns Controller name string
*/
private getControllerName(routeInfo: any): string {
if (!routeInfo || !routeInfo.controller) return 'unknown';
return routeInfo.controller;
}
/**
* Extracts route information from a request
*
* @param request - HTTP request object
* @returns Route path string or undefined
*/
private extractRouteInfo(request: any): string | undefined {
// Try to get route from NestJS route information
if (request.route?.path) {
return request.route.path;
}
// Fallback to path if available
return request.path;
}
/**
* Gets the status code from an exception with fallback
*
* @param exception - Exception object
* @returns HTTP status code
*/
private getStatusCode(exception: any): number {
// Try to get status from HttpException
if (exception instanceof HttpException) {
return exception.getStatus();
}
// Check for custom status property
if (exception.status && typeof exception.status === 'number') {
return exception.status;
}
// Default to 500 Internal Server Error
return 500;
}
/**
* Sanitizes stack traces to remove sensitive information
*
* @param stack - Error stack trace
* @returns Sanitized stack trace
*/
private sanitizeStack(stack?: string): string | undefined {
if (!stack) return undefined;
// Remove absolute file paths, keep only relative paths
return stack
.split('\n')
.map((line) => {
// Remove absolute paths containing user directories
return line.replace(/\((\/Users\/|\/home\/|[A-Z]:\\Users\\)[^)]+/g, '(project/');
})
.join('\n')
.slice(0, 2000); // Limit stack trace length
}
}
@@ -0,0 +1,336 @@
// Nestjs imports
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor, Optional } from '@nestjs/common';
import { ContextType, HttpArgumentsHost, RpcArgumentsHost, WsArgumentsHost } from '@nestjs/common/interfaces';
// Rxjs imports
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PosthogEventInterceptorOptions } from './posthog.interfaces';
import { PosthogService } from './posthog.service';
import { SanitizerUtil } from './utils';
import { POSTHOG_MODULE_OPTIONS } from './posthog.constants';
/**
* Interceptor for capturing and tracking events through PostHog analytics.
* Automatically tracks route access and performance metrics.
* Sanitizes sensitive data before sending to PostHog.
*/
@Injectable()
export class PosthogEventInterceptor implements NestInterceptor {
/**
* PostHog service instance for tracking events
*/
protected readonly client: PosthogService = PosthogService.PosthogServiceInstance();
private readonly options?: PosthogEventInterceptorOptions;
/**
* @param options - Configuration options for the interceptor
*/
constructor(@Optional() @Inject(POSTHOG_MODULE_OPTIONS) options?: PosthogEventInterceptorOptions) {
this.options = options;
}
/**
* Intercepts requests and tracks events when they occur
*
* @param context - Execution context of the current request
* @param next - Call handler for the request pipeline
* @returns Observable of the response stream
*/
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
// Track the request start event
this.captureRequestStart(context);
return next.handle().pipe(
tap(
// Success handler
(response) => {
const duration = Date.now() - startTime;
this.captureRequestComplete(context, response, duration);
},
// Error handler - note that errors are already handled by the PosthogErrorInterceptor
// This is just to track that a request completed with an error
(error) => {
const duration = Date.now() - startTime;
this.captureRequestError(context, error, duration);
}
)
);
}
/**
* Routes event capture based on context type (HTTP, RPC, WebSocket)
*
* @param context - Execution context of the current request
* @param eventName - Name of the event to capture
* @param properties - Additional properties to include with the event
*/
protected captureEvent(context: ExecutionContext, eventName: string, properties: Record<string, any> = {}) {
switch (context.getType<ContextType>()) {
case 'http':
return this.captureHttpEvent(context.switchToHttp(), eventName, properties);
case 'rpc':
return this.captureRpcEvent(context.switchToRpc(), eventName, properties);
case 'ws':
return this.captureWsEvent(context.switchToWs(), eventName, properties);
default:
console.warn(`Unknown context type encountered while capturing event ${eventName}`);
}
}
/**
* Captures HTTP request events with comprehensive request context
*
* @param http - HTTP arguments host containing request data
* @param eventName - Name of the event to track
* @param additionalProperties - Additional event properties
*/
private captureHttpEvent(
http: HttpArgumentsHost,
eventName: string,
additionalProperties: Record<string, any> = {}
): void {
const request = http.getRequest();
const userId = this.extractUserId(request);
// Skip tracking if this endpoint should be ignored
if (this.shouldIgnoreEndpoint(request.method, request.originalUrl || request.url)) {
return;
}
const eventProperties = {
request_method: request.method,
request_url: request.originalUrl || request.url,
request_path: request.path,
request_query: SanitizerUtil.sanitizeObject(request.query),
user_agent: request.headers['user-agent'],
referer: request.headers.referer || request.headers.referrer,
ip_address: request.ip,
user_id: request.user?.id || request.user?.userId,
route: this.extractRouteFromRequest(request),
...additionalProperties,
$timestamp: new Date().toISOString()
};
// Track the event
this.client.track(eventName, userId, eventProperties);
}
/**
* Captures RPC events with message context
*
* @param rpc - RPC arguments host
* @param eventName - Name of the event to track
* @param additionalProperties - Additional event properties
*/
private captureRpcEvent(
rpc: RpcArgumentsHost,
eventName: string,
additionalProperties: Record<string, any> = {}
): void {
const ctx = rpc.getContext();
const pattern = typeof ctx.getPattern === 'function' ? ctx.getPattern() : 'unknown-pattern';
const clientId = this.extractRpcClientId(ctx) || 'system';
const eventProperties = {
rpc_pattern: pattern,
rpc_context: SanitizerUtil.sanitizeObject(ctx),
...additionalProperties,
$timestamp: new Date().toISOString()
};
this.client.track(eventName, clientId, eventProperties);
}
/**
* Captures WebSocket events with client context
*
* @param ws - WebSocket arguments host
* @param eventName - Name of the event to track
* @param additionalProperties - Additional event properties
*/
private captureWsEvent(
ws: WsArgumentsHost,
eventName: string,
additionalProperties: Record<string, any> = {}
): void {
const client = ws.getClient();
const data = ws.getData();
const eventType = typeof data === 'object' && data.event ? data.event : 'unknown-event';
const clientId = client.id || this.extractWsClientId(client) || 'websocket-client';
const eventProperties = {
ws_event_type: eventType,
ws_client_id: client.id,
...additionalProperties,
$timestamp: new Date().toISOString()
};
this.client.track(eventName, clientId, eventProperties);
}
/**
* Captures the start of a request processing pipeline
*
* @param context - Execution context of the current request
*/
protected captureRequestStart(context: ExecutionContext): void {
this.captureEvent(context, 'request_started', {
handler: this.getHandlerName(context),
timestamp_start: Date.now()
});
}
/**
* Captures the successful completion of a request
*
* @param context - Execution context of the current request
* @param response - The response being sent back
* @param duration - Duration of the request processing in milliseconds
*/
protected captureRequestComplete(context: ExecutionContext, response: any, duration: number): void {
let statusCode = 200;
let responseSize = 0;
// Try to extract status code and response size based on context type
if (context.getType() === 'http') {
const httpResponse = context.switchToHttp().getResponse();
statusCode = httpResponse.statusCode;
// Estimate response size if possible
if (response && typeof response === 'object') {
try {
responseSize = JSON.stringify(response).length;
} catch (e) {
// Ignore serialization errors
}
}
}
this.captureEvent(context, 'request_completed', {
handler: this.getHandlerName(context),
duration_ms: duration,
status_code: statusCode,
response_size_bytes: responseSize,
timestamp_end: Date.now()
});
}
/**
* Captures error events during request processing
* Note: This complements the PosthogErrorInterceptor by tracking timing metrics
*
* @param context - Execution context of the current request
* @param error - The error that occurred
* @param duration - Duration until the error occurred in milliseconds
*/
protected captureRequestError(context: ExecutionContext, error: any, duration: number): void {
let statusCode = error?.getStatus?.() || 500;
this.captureEvent(context, error?.name || 'request_error', {
handler: this.getHandlerName(context),
duration_ms: duration,
error_type: error?.name || 'Unknown',
status_code: statusCode,
timestamp_error: Date.now()
});
}
/**
* Extracts the handler name from the execution context
*
* @param context - Execution context
* @returns The name of the handler being executed
*/
private getHandlerName(context: ExecutionContext): string {
const handler = context.getHandler();
const controller = context.getClass();
return `${controller.name}.${handler.name}`;
}
/**
* Extracts a route from an HTTP request
*
* @param request - HTTP request object
* @returns Route path with parameter placeholders
*/
private extractRouteFromRequest(request: any): string {
// Try to access NestJS route information if available
if (request.route && request.route.path) {
return request.route.path;
}
// Fallback to the raw URL
return request.originalUrl || request.url;
}
/**
* Determines if an endpoint should be ignored for tracking
*
* @param method - HTTP method
* @param url - URL of the request
* @returns Boolean indicating if the endpoint should be ignored
*/
private shouldIgnoreEndpoint(method: string, url: string): boolean {
// Skip tracking for health checks and other monitoring endpoints by default
const defaultIgnoredPaths = ['/health', '/metrics', '/favicon.ico', '/ping'];
// Use user-configured ignored paths if available
const ignoredPaths = this.options?.ignoredPaths || defaultIgnoredPaths;
return ignoredPaths.some((path: string) => url.startsWith(path));
}
/**
* Extracts a user ID from various possible locations in the request
*
* @param request - HTTP request object
* @returns User ID string or 'anonymous' if not found
*/
private extractUserId(request: any): string {
// Check common user ID locations
return (
request.user?.id ||
request.user?.userId ||
request.headers['x-user-id'] ||
request.cookies?.userId ||
request.ip ||
'anonymous'
);
}
/**
* Extracts a client ID from RPC context
*
* @param ctx - RPC context
* @returns Client ID string or null if not found
*/
private extractRpcClientId(ctx: any): string | null {
return ctx.clientId || (ctx.args && ctx.args[0] && ctx.args[0].user && ctx.args[0].user.id) || null;
}
/**
* Extracts a client ID from WebSocket client
*
* @param client - WebSocket client
* @returns Client ID string or null if not found
*/
private extractWsClientId(client: any): string | null {
if (!client) return null;
// Try to extract user ID from handshake data
if (client.handshake && client.handshake.query && client.handshake.query.userId) {
return client.handshake.query.userId;
}
// Try to extract from auth data
if (client.handshake && client.handshake.auth && client.handshake.auth.userId) {
return client.handshake.auth.userId;
}
return null;
}
}
@@ -0,0 +1,34 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { PosthogService } from './posthog.service';
@Injectable()
export class PosthogRequestMiddleware implements NestMiddleware {
constructor(private readonly posthog: PosthogService) {}
/**
* Intercepts incoming requests to capture basic request information
* Identifies the user/device and sets initial event properties
* @param req - Incoming HTTP request
* @param res - HTTP response
* @param next - Next middleware function
*/
use(req: Request, res: Response, next: NextFunction) {
// Capture essential request properties as user identification
try {
this.posthog.identify(req.ip || 'anonymous', {
$current_url: req.originalUrl,
$referrer: req.get('referer'),
$user_agent: req.get('user-agent'),
$host: req.get('host'),
$pathname: req.path,
ip: req.ip,
http_method: req.method
});
} catch (error) {
console.debug('PostHog analytics error:', error);
}
next();
}
}
@@ -0,0 +1,33 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { PosthogService } from './posthog.service';
@Injectable()
export class PosthogTraceMiddleware implements NestMiddleware {
constructor(private readonly posthog: PosthogService) {}
/**
* Tracks request performance metrics and status codes
* Attaches to response finish event to capture final timing
* @param req - Incoming HTTP request
* @param res - HTTP response
* @param next - Next middleware function
*/
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
const { method, path } = req;
res.on('finish', () => {
const duration = Date.now() - start;
this.posthog.track('http_request', req.ip || 'unknown', {
method,
path,
status_code: res.statusCode,
duration_ms: duration,
$timestamp: new Date(start).toISOString()
});
});
next();
}
}
@@ -0,0 +1,5 @@
/**
* Injection token for PosthogModuleOptions.
*/
export const POSTHOG_MODULE_OPTIONS = 'POSTHOG_MODULE_OPTIONS';
export const POSTHOG_TOKEN = 'POSTHOG_TOKEN';
@@ -0,0 +1,167 @@
import { ModuleMetadata, Type } from '@nestjs/common';
/**
* Configuration options for the PostHog module
*/
export interface PosthogModuleOptions {
apiKey: string;
/** PostHog API host URL (defaults to https://app.posthog.com) */
apiHost?: string;
/** Whether to enable error tracking (defaults to true) */
enableErrorTracking?: boolean;
/** Number of events to queue before sending (defaults to 20) */
flushAt?: number;
/** Maximum milliseconds to wait before sending data (defaults to 10000) */
flushInterval?: number;
/** Personal API key for PostHog */
personalApiKey?: string;
/** Whether to automatically capture clicks and form submissions (defaults to false) */
autocapture?: boolean;
/** Whether to use a mock client for testing (defaults to false) */
mock?: boolean;
}
export interface PosthogModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useFactory?: (...args: unknown[]) => Promise<PosthogModuleOptions> | PosthogModuleOptions;
inject?: (string | symbol | Type<any>)[];
useClass?: Type<PosthogOptionsFactory>;
useExisting?: Type<PosthogOptionsFactory>;
isGlobal?: boolean;
}
export interface PosthogOptionsFactory {
createPosthogOptions(): Promise<PosthogModuleOptions> | PosthogModuleOptions;
}
// Common PostHog property types
export interface PosthogEventProperties {
// Standard PostHog properties (prefixed with $)
$ip?: string;
$timestamp?: string;
$set?: Record<string, any>;
$set_once?: Record<string, any>;
// Custom properties
[key: string]: any;
}
export interface PosthogEvent {
name: string;
distinctId: string;
properties: PosthogEventProperties;
}
export interface PosthogInterceptorOptions {
/**
* Filters to determine which exceptions should not be reported
*/
filters?: PosthogInterceptorOptionsFilter[];
/**
* Indicates if tags should be included for better categorization
* @default true
*/
includeTags?: boolean;
/**
* Limits the size of captured objects (in characters)
* @default 10000
*/
maxObjectSize?: number;
/**
* List of header keys to never capture (for privacy reasons)
*/
sensitiveHeaders?: string[];
/**
* Detail level for error capturing
* - basic: error message and status code only
* - standard: basic details plus request context
* - detailed: all available information
* @default 'standard'
*/
detailLevel?: 'basic' | 'standard' | 'detailed';
}
export interface PosthogInterceptorOptionsFilter {
type: any;
filter?: (exception: any) => boolean;
}
/**
* Custom properties that can be added to all events
*/
export interface PosthogCustomProperties {
// Add some common properties your application uses
environment?: string;
version?: string;
userRole?: string;
// Allow additional custom properties
[key: string]: any;
}
/**
* Configuration options for the PostHogEventInterceptor
*/
export interface PosthogEventInterceptorOptions {
/**
* Paths that should be ignored for event tracking
*/
ignoredPaths?: string[];
/**
* Whether to track request performance metrics
*/
trackPerformance?: boolean;
/**
* Whether to track user information when available
*/
trackUserInfo?: boolean;
/**
* Custom properties to add to all events
*/
customProperties?: PosthogCustomProperties;
}
/**
* Parses and validates PostHog options
* @param options Raw PostHog options
* @returns Normalized PostHog options
*/
export const parsePosthogOptions = (options: PosthogModuleOptions): PosthogModuleOptions => {
// Validate required options
if (!options.apiKey) {
throw new Error('PostHog API key is required');
}
// Validate numeric values
const flushAt = options.flushAt ?? 20;
if (flushAt <= 0) {
throw new Error('flushAt must be a positive number');
}
const flushInterval = options.flushInterval ?? 10000;
if (flushInterval <= 0) {
throw new Error('flushInterval must be a positive number');
}
return {
apiKey: options.apiKey,
apiHost: options.apiHost || 'https://app.posthog.com',
enableErrorTracking: options.enableErrorTracking ?? true,
flushAt,
flushInterval,
personalApiKey: options.personalApiKey,
autocapture: options.autocapture ?? false,
mock: options.mock ?? false
};
};
@@ -0,0 +1,29 @@
import { Module, DynamicModule } from '@nestjs/common';
import { PosthogCoreModule } from './posthog-core.module';
import { PosthogModuleOptions, PosthogModuleAsyncOptions } from './posthog.interfaces';
/**
* Entry module for PostHog integration.
*/
@Module({})
export class PosthogModule {
/**
* Static method to create a dynamic module with synchronous configuration.
*/
public static forRoot(options: PosthogModuleOptions): DynamicModule {
return {
module: PosthogModule,
imports: [PosthogCoreModule.forRoot(options)]
};
}
/**
* Static method to create a dynamic module with asynchronous configuration.
*/
public static forRootAsync(options: PosthogModuleAsyncOptions): DynamicModule {
return {
module: PosthogModule,
imports: [PosthogCoreModule.forRootAsync(options)]
};
}
}
@@ -0,0 +1,84 @@
import { APP_INTERCEPTOR } from '@nestjs/core';
import { MiddlewareConsumer, NestModule } from '@nestjs/common';
import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin';
import { PosthogModule } from './posthog.module';
import { parsePosthogOptions, PosthogModuleOptions } from './posthog.interfaces';
import { PosthogEventInterceptor } from './posthog-event.interceptor';
import { PosthogErrorInterceptor } from './posthog-error.interceptor';
import { PosthogRequestMiddleware } from './posthog-request.middleware';
import { PosthogTraceMiddleware } from './posthog-trace.middleware';
import { POSTHOG_MODULE_OPTIONS } from './posthog.constants';
@Plugin({
imports: [
PosthogModule.forRootAsync({
useFactory: () => parsePosthogOptions(PosthogPlugin.options),
inject: []
})
],
providers: [
{
provide: POSTHOG_MODULE_OPTIONS,
useValue: PosthogPlugin.options
},
{
provide: APP_INTERCEPTOR,
useClass: PosthogErrorInterceptor
},
{
provide: APP_INTERCEPTOR,
useClass: PosthogEventInterceptor
}
]
})
export class PosthogPlugin implements NestModule, IOnPluginBootstrap, IOnPluginDestroy {
private logEnabled = true;
static options: PosthogModuleOptions;
/**
* Configures PostHog middlewares for all routes
* @param consumer The middleware consumer
*/
configure(consumer: MiddlewareConsumer) {
if (this.shouldEnableTracking()) {
consumer.apply(PosthogRequestMiddleware).forRoutes('*').apply(PosthogTraceMiddleware).forRoutes('*');
}
}
/**
* Called when plugin is initialized
*/
onPluginBootstrap(): void {
if (this.logEnabled) {
console.log('🚀 PostHog plugin initialized');
}
}
/**
* Called when plugin is destroyed
*/
onPluginDestroy(): void {
if (this.logEnabled) {
console.log('🛑 PostHog plugin destroyed');
}
}
/**
* Initialize plugin with options
* @param options PostHog configuration options
* @returns The plugin instance
*/
static init(options: PosthogModuleOptions): typeof PosthogPlugin {
this.options = parsePosthogOptions(options);
return this;
}
/**
* Determines if tracking should be enabled
* @returns boolean indicating if tracking is enabled
*/
private shouldEnableTracking(): boolean {
return !PosthogPlugin.options.mock && !!PosthogPlugin.options.apiKey;
}
}
@@ -0,0 +1,10 @@
import { PosthogModuleOptions } from './posthog.interfaces';
import { POSTHOG_TOKEN } from './posthog.constants';
import { PosthogService } from './posthog.service';
export function createPosthogProviders(options: PosthogModuleOptions) {
return {
provide: POSTHOG_TOKEN,
useFactory: () => new PosthogService(options)
};
}
@@ -0,0 +1,143 @@
import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import { PosthogModuleOptions } from './posthog.interfaces';
import { POSTHOG_MODULE_OPTIONS } from './posthog.constants';
import { PostHog } from 'posthog-node';
/**
* Service that provides an interface to the PostHog analytics platform.
* Follows the singleton pattern to ensure only one instance exists.
* Implements OnModuleDestroy for proper cleanup when the module is destroyed.
*/
@Injectable()
export class PosthogService implements OnModuleDestroy {
private readonly logger = new Logger(PosthogService.name);
private client: PostHog | null = null;
private static instance: PosthogService;
/**
* Creates a new PosthogService instance.
* Initializes the singleton instance if it doesn't already exist.
*
* @param options - Configuration options for the PostHog client
*/
constructor(
@Inject(POSTHOG_MODULE_OPTIONS)
private readonly options: PosthogModuleOptions
) {
// Initialize the static instance if it doesn't exist
if (!PosthogService.instance) {
PosthogService.instance = this;
this.initClient();
}
}
/**
* Gets the singleton instance of the PosthogService.
*
* @returns The singleton instance of PosthogService
*/
public static PosthogServiceInstance(): PosthogService {
if (!PosthogService.instance) {
throw new Error('PosthogService instance not initialized');
}
return PosthogService.instance;
}
/**
* Gets the underlying PostHog client instance.
*
* @returns The PostHog client instance or null if not initialized
*/
public instance(): PostHog | null {
return this.client;
}
/**
* Initializes the PostHog client with the provided configuration.
* If the API key is missing, analytics will be disabled.
*/
private initClient() {
if (!this.options.apiKey) {
this.logger.warn('PostHog API key is missing. Analytics will be disabled.');
return;
}
this.client = new PostHog(this.options.apiKey, {
host: this.options.apiHost || 'https://app.posthog.com',
enableExceptionAutocapture: this.options.enableErrorTracking,
flushAt: this.options.flushAt || 20,
flushInterval: this.options.flushInterval || 10000,
personalApiKey: this.options.personalApiKey
});
this.logger.log('PostHog client initialized');
}
/**
* Tracks a custom event for a specific user.
*
* @param event - The name of the event to track
* @param distinctId - The unique identifier for the user
* @param properties - Optional additional properties to associate with the event
*/
public track(event: string, distinctId: string, properties?: Record<string, any>) {
if (!this.client) return;
this.client.capture({
event,
distinctId,
properties
});
}
/**
* Captures an exception or error event for a specific user.
* Uses PostHog's captureException method to track errors.
*
* @param exception - The error or exception to capture
* @param distinctId - The unique identifier for the user
* @param properties - Optional additional properties describing the error
*/
public captureException(exception: any, distinctId: string, properties?: Record<string, any>) {
if (!this.client) return;
this.client.captureException({
exception,
distinctId,
properties
});
}
/**
* Identifies a user with additional traits or properties.
*
* @param distinctId - The unique identifier for the user
* @param properties - Optional user traits or properties to associate with this user
*/
public identify(distinctId: string, properties?: Record<string, any>) {
if (!this.client) return;
this.client.identify({
distinctId,
properties
});
}
/**
* Gracefully shuts down the PostHog client, ensuring all queued events are sent.
*
* @returns Promise that resolves when shutdown is complete
*/
public async shutdown(): Promise<void> {
if (!this.client) return;
await this.client.shutdown();
this.logger.log('PostHog client shutdown complete');
}
/**
* Lifecycle hook called when the NestJS module is being destroyed.
* Automatically calls the shutdown method to ensure proper cleanup.
*
* @returns Promise that resolves when shutdown is complete
*/
async onModuleDestroy() {
await this.shutdown();
}
}
@@ -0,0 +1,3 @@
import { PosthogModuleOptions } from './posthog.interfaces';
export interface PosthogPluginOptions extends PosthogModuleOptions {}
@@ -0,0 +1 @@
export * from './sanitizer.util';
@@ -0,0 +1,33 @@
export class SanitizerUtil {
private static readonly sensitiveFields = ['password', 'token', 'secret', 'credit_card', 'ssn'];
private static readonly sensitiveHeaders = ['authorization', 'cookie', 'api-key'];
static sanitizeHeaders(headers: Record<string, any>): Record<string, any> {
const sanitized = { ...headers };
for (const header of this.sensitiveHeaders) {
const headerLower = header.toLowerCase();
for (const key in sanitized) {
if (key.toLowerCase() === headerLower) {
sanitized[key] = '[REDACTED]';
}
}
}
return sanitized;
}
static sanitizeObject(obj: any): any {
if (!obj || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map((item) => this.sanitizeObject(item));
const result = { ...obj };
for (const key in result) {
if (this.sensitiveFields.includes(key.toLowerCase())) {
result[key] = '[REDACTED]';
} else if (typeof result[key] === 'object') {
result[key] = this.sanitizeObject(result[key]);
}
}
return result;
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"],
"target": "es2021",
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}
+1
View File
@@ -46,6 +46,7 @@
"@gauzy/plugin-public-layout-ui": ["./packages/plugins/public-layout-ui/src/index.ts"],
"@gauzy/plugin-registry": ["./packages/plugins/registry/src/index.ts"],
"@gauzy/plugin-sentry": ["./packages/plugins/sentry-tracing/src/index.ts"],
"@gauzy/plugin-posthog": ["./packages/plugins/posthog/src/index.ts"],
"@gauzy/plugin-videos": ["./packages/plugins/videos/src/index.ts"],
"@gauzy/plugin-videos-ui": ["./packages/plugins/videos-ui/src/index.ts"],
"@gauzy/ui-auth": ["./packages/ui-auth/src/index.ts"],
+7
View File
@@ -32328,6 +32328,13 @@ postgres-range@^1.1.1:
resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863"
integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==
posthog-node@^4.11.3:
version "4.11.3"
resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-4.11.3.tgz#b01c3b32b524bfdb1c1e2aef256b36933e6b91c0"
integrity sha512-Ye7d9ZJX1reWP9084Gm9+O7NnvlW7LnbU09GzlsSdD0HzGTfxeKTQZsl9h1+CDHhfvpdWJRm8uc91/aDmXzaCA==
dependencies:
axios "^1.8.2"
preact@~10.12.1:
version "10.12.1"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21"