mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
[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:
@@ -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}'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# yarn
|
||||
yarn-error.log
|
||||
|
||||
# misc
|
||||
npm-debug.log
|
||||
dist
|
||||
build
|
||||
@@ -0,0 +1,4 @@
|
||||
# .npmignore
|
||||
|
||||
src/
|
||||
node_modules/
|
||||
@@ -0,0 +1,3 @@
|
||||
# Changelog for @gauzy/plugin-posthog
|
||||
|
||||
## [Unreleased]
|
||||
@@ -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
|
||||
```
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user