diff --git a/packages/analytics-plugin-thrivestack/README.md b/packages/analytics-plugin-thrivestack/README.md new file mode 100644 index 00000000..e426ce20 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/README.md @@ -0,0 +1,195 @@ +# ThriveStack Plugin for Analytics + +Integration with [ThriveStack](https://thrivestack.ai) for the [`analytics`](https://www.npmjs.com/package/analytics) package. + +## Installation + +```bash +npm install analytics @analytics/thrivestack +``` + +## Usage + +```js +import Analytics from 'analytics' +import thriveStackPlugin from '@analytics/thrivestack' + +const analytics = Analytics({ + app: 'TS-Analytic-app', + plugins: [ + thriveStackPlugin({ + apiKey: 'your-thrivestack-api-key', // ⚠️ REQUIRED + source: 'website', // ⚠️ REQUIRED + + options: { + // Additional configuration (optional) + debug: true, + respectDoNotTrack: true, + trackClicks: true, + trackForms: true + } + }) + ] +}) + +// Track a page view +analytics.page() + +// Track an event +analytics.track('itemPurchased', { + price: 29.99, + item: 'Premium Subscription' +}) + +// Identify a user +analytics.identify('user-123', { + email: 'user@example.com', + name: 'John Doe' +}) + +// Reset user and group data +analytics.reset() + + +Basic Configuration +When initializing the ThriveStack plugin, two parameters are critical for proper functionality: + +apiKey (REQUIRED): Your unique ThriveStack API key. Without this, all tracking calls will fail. +source (REQUIRED): Must be one of exactly two allowed values: + +'marketing': For tracking marketing-related analytics +'product': For tracking product usage analytics + +⚠️ Important: No other values are accepted for the source parameter! + + +## Configuration Options + +The ThriveStack plugin accepts the following configuration options: + +| Option | Description | Required | Default Value | Allowed Values | +|:------|:------------|:---------|:--------------|:---------------| +| `apiKey` | Your ThriveStack API key | Yes | - | Valid API key | +| `source` | Source identifier | Yes | - | Only `'marketing'` or `'product'` | +| `respectDoNotTrack` | Whether to respect DNT browser setting | No | `true` | `true`, `false` | +| `trackClicks` | Automatically track click events | No | `false` | `true`, `false` | +| `trackForms` | Automatically track form submissions | No | `false` | `true`, `false` | +| `enableConsent` | Enable consent management | No | `false` | `true`, `false` | +| `defaultConsent` | Default consent value | No | `false` | `true`, `false` | +| `batchSize` | Number of events to batch together | No | `10` | Positive number | +| `batchInterval` | Interval in ms for processing event queue | No | `2000` | Positive number | +| `options` | Additional options object | No | `{}` | Object | + +> ⚠️ **Source Parameter Limitation**: The `source` parameter must be set to either `'marketing'` or `'product'`. Any other value will result in validation errors and tracking will fail. + +## Methods + +### Core Methods + +The ThriveStack plugin implements these core Analytics API methods: + +- **page**: Tracks page views +- **track**: Tracks custom events +- **identify**: Identifies users and their traits +- **reset**: Resets user and group data + +### Custom Methods + +The ThriveStack plugin also exposes these additional methods: + +```js +// Get the ThriveStack plugin instance +const thrivestack = analytics.plugins.thrivestack + +// Group identification +thrivestack.groupIdentify( + 'group-123', // Group ID + { // Group traits + name: 'Engineering Team', + group_type: 'department' + }, + { // Options + userId: 'user-123' + }, + (error, result) => { + // Optional callback + if (error) console.error(error) + else console.log(result) + } +) + +// Set API configuration +thrivestack.setApiConfig({ + apiKey: 'new-api-key', + source: 'new-source' +}) + +// Set user consent preferences +thrivestack.setConsent('analytics', true) +thrivestack.setConsent('marketing', false) + +// Enable debug mode +thrivestack.enableDebugMode() + +// Get device ID +const deviceId = thrivestack.getDeviceId() + +// Get session ID +const sessionId = thrivestack.getSessionId() + +// Get user ID +const userId = thrivestack.getUserId() + +// Get group ID +const groupId = thrivestack.getGroupId() + +// Get source +const source = thrivestack.getSource() + +// Set source +thrivestack.setSource('mobile-app') + +// Get UTM parameters +const utmParams = thrivestack.getUtmParameters() +``` + +## Automatic Event Tracking + +When `trackClicks` and `trackForms` options are enabled, the ThriveStack plugin will automatically track: + +1. Click events on page elements +2. Form submissions and abandoned forms + +These events are sent with rich contextual data including: + +- Element information (for clicks) +- Form completion percentage (for forms) +- Page information +- UTM parameters (when available) +- Device and session IDs + +## Auto-Consent and Privacy + +The ThriveStack plugin respects user privacy settings: + +- When `respectDoNotTrack` is enabled, the plugin checks the browser's DNT setting +- Consent can be managed per category with the `setConsent` method +- Default consent behavior can be configured with the `defaultConsent` option + +## Event Batching + +Events are automatically batched according to the configured `batchSize` and `batchInterval` settings to optimize network requests. + +## Browser Support + +The ThriveStack plugin is compatible with all modern browsers: + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) +- IE11 (with appropriate polyfills) + +## License + +MIT \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/index.d.ts b/packages/analytics-plugin-thrivestack/index.d.ts new file mode 100644 index 00000000..ca77ca6a --- /dev/null +++ b/packages/analytics-plugin-thrivestack/index.d.ts @@ -0,0 +1,167 @@ +// Type definitions for @analytics/thrivestack +// Project: https://github.com/DavidWells/analytics/tree/master/packages/analytics-plugin-thrivestack + +import { AnalyticsPlugin } from 'analytics' + +/** + * ThriveStack plugin configuration options + */ +export interface ThriveStackConfig { + /** ThriveStack API key (required) */ + apiKey: string; + /** API endpoint */ + apiEndpoint?: string; + /** Whether to respect DNT browser setting */ + respectDoNotTrack?: boolean; + /** Automatically track click events */ + trackClicks?: boolean; + /** Automatically track form submissions */ + trackForms?: boolean; + /** Enable consent management */ + enableConsent?: boolean; + /** Default consent value */ + defaultConsent?: boolean; + /** Source identifier */ + source?: string; + /** Number of events to batch together */ + batchSize?: number; + /** Interval in ms for processing event queue */ + batchInterval?: number; + /** Additional options for ThriveStack */ + options?: ThriveStackOptions; +} + +/** + * ThriveStack options + */ +export interface ThriveStackOptions { + /** Enable debug mode */ + debug?: boolean; + /** Custom API URL */ + apiUrl?: string; + /** Timestamp override */ + timestamp?: string; + /** User ID */ + userId?: string; + /** Group ID */ + groupId?: string; + /** Source identifier */ + source?: string; + /** Any other custom options */ + [key: string]: any; +} + +/** + * Group traits + */ +export interface GroupTraits { + /** Group name */ + name?: string; + /** Group type */ + group_type?: string; + /** Any other traits */ + [key: string]: any; +} + +/** + * Group identify options + */ +export interface GroupIdentifyOptions { + /** User ID */ + userId?: string; + /** User ID (alternative format) */ + user_id?: string; + /** Timestamp */ + timestamp?: string; + /** Source identifier */ + source?: string; + /** Any other options */ + [key: string]: any; +} + +/** + * API config options + */ +export interface ApiConfigOptions { + /** API key */ + apiKey?: string; + /** Custom API URL */ + apiUrl?: string; + /** Source identifier */ + source?: string; + /** Any other config options */ + [key: string]: any; +} + +/** + * UTM parameters + */ +export interface UtmParameters { + /** UTM campaign */ + utm_campaign: string | null; + /** UTM medium */ + utm_medium: string | null; + /** UTM source */ + utm_source: string | null; + /** UTM term */ + utm_term: string | null; + /** UTM content */ + utm_content: string | null; +} + +/** + * ThriveStack Plugin API methods + */ +export interface ThriveStackMethods { + /** Group identify method */ + groupIdentify( + groupId: string, + traits?: GroupTraits, + options?: GroupIdentifyOptions, + callback?: (error: Error | null, result?: any) => void + ): Promise; + + /** Set API configuration */ + setApiConfig(config?: ApiConfigOptions): boolean; + + /** Set user consent settings */ + setConsent(category: 'functional' | 'analytics' | 'marketing', enabled: boolean): boolean; + + /** Enable debug mode */ + enableDebugMode(): boolean; + + /** Get device ID */ + getDeviceId(): string | null; + + /** Get session ID */ + getSessionId(): string | null; + + /** Get user ID */ + getUserId(): string | null; + + /** Get group ID */ + getGroupId(): string | null; + + /** Get source */ + getSource(): string | null; + + /** Set source */ + setSource(source: string): boolean; + + /** Get UTM parameters */ + getUtmParameters(): UtmParameters; + + /** Get ThriveStack instance (escape hatch) */ + getThriveStackInstance(): any; +} + +/** + * ThriveStack Analytics plugin for browser & node + * @param config - Plugin configuration + * @returns Analytics plugin + */ +declare function thriveStackPlugin(config: ThriveStackConfig): AnalyticsPlugin & { + methods: ThriveStackMethods +}; + +export default thriveStackPlugin \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/index.test.ts b/packages/analytics-plugin-thrivestack/index.test.ts new file mode 100644 index 00000000..851b2efc --- /dev/null +++ b/packages/analytics-plugin-thrivestack/index.test.ts @@ -0,0 +1,135 @@ +/** + * Test file for ThriveStack plugin + */ + +import test from 'ava' +import sinon from 'sinon' +import { createScriptLoader } from '@analytics/script-loader' +import thriveStackPlugin from '../index' + +// Stub the script loader +sinon.stub(createScriptLoader) + +// Mock global window +global.window = { + thrivestack: { + init: sinon.spy(), + page: sinon.spy(), + track: sinon.spy(), + identify: sinon.spy() + } +} + +// Plugin configuration for tests +const config = { + apiKey: 'test-api-key', + options: { + debug: true + } +} + +// Create plugin for testing +const thriveStack = thriveStackPlugin(config) + +test('ThriveStack plugin configuration', t => { + t.is(thriveStack.name, 'thrivestack') + t.deepEqual(thriveStack.config, { + name: 'thrivestack', + apiKey: 'test-api-key', + options: { + debug: true + } + }) +}) + +test('ThriveStack plugin initialize method', t => { + const analyticsSpy = { + config: config, + instance: {} + } + + thriveStack.initialize(analyticsSpy) + + // Verify script loader was called + t.true(createScriptLoader.called) +}) + +test('ThriveStack plugin page method', t => { + const pagePayload = { + properties: { + url: '/test', + title: 'Test Page' + } + } + + thriveStack.page({ payload: pagePayload }) + + // Verify page was called with expected params + t.true(global.window.thrivestack.page.calledOnce) + t.true(global.window.thrivestack.page.calledWith(pagePayload.properties)) +}) + +test('ThriveStack plugin track method', t => { + const trackPayload = { + event: 'test_event', + properties: { + value: 100, + category: 'test' + } + } + + thriveStack.track({ payload: trackPayload }) + + // Verify track was called with expected params + t.true(global.window.thrivestack.track.calledOnce) + t.true(global.window.thrivestack.track.calledWith( + trackPayload.event, + trackPayload.properties + )) +}) + +test('ThriveStack plugin identify method', t => { + const identifyPayload = { + userId: 'test-user-123', + traits: { + name: 'Test User', + email: 'test@example.com' + } + } + + thriveStack.identify({ payload: identifyPayload }) + + // Verify identify was called with expected params + t.true(global.window.thrivestack.identify.calledOnce) + t.true(global.window.thrivestack.identify.calledWith( + identifyPayload.userId, + identifyPayload.traits + )) +}) + +test('ThriveStack plugin loaded method', t => { + // Should return true if window.thrivestack exists + t.true(thriveStack.loaded()) + + // Modify global for testing + const original = global.window.thrivestack + global.window.thrivestack = null + + // Should return false if window.thrivestack doesn't exist + t.false(thriveStack.loaded()) + + // Restore global + global.window.thrivestack = original +}) + +test('ThriveStack plugin exposes API methods', t => { + t.is(typeof thriveStack.methods.getEvents, 'function') + t.is(typeof thriveStack.methods.getPageVisits, 'function') + t.is(typeof thriveStack.methods.getUserData, 'function') + t.is(typeof thriveStack.methods.getDashboardStats, 'function') + t.is(typeof thriveStack.methods.getThriveStackInstance, 'function') + + // Test getThriveStackInstance + const instance = thriveStack.methods.getThriveStackInstance() + t.deepEqual(instance, global.window.thrivestack) +}) \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js new file mode 100644 index 00000000..0d5b7243 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -0,0 +1,689 @@ +function thriveStackPlugin(pluginConfig = {}) { + if (!pluginConfig.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + window._tsq = window._tsq || []; + window.ThriveStack = window.ThriveStack || null; + let initCompleted = false; + let API_KEY = pluginConfig.apiKey; + const options = { + respectDoNotTrack: pluginConfig.respectDoNotTrack !== false, + trackClicks: pluginConfig.trackClicks === true, + trackForms: pluginConfig.trackForms === true, + enableConsent: pluginConfig.enableConsent === true, + source: pluginConfig.source || '', + defaultConsent: pluginConfig.defaultConsent === true, + batchSize: pluginConfig.batchSize || 10, + batchInterval: pluginConfig.batchInterval || 2000, + ...pluginConfig.options + }; + const initComplete = i => { + initCompleted = true; + console.log('ThriveStack initialization completed'); + }; + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Cookie utility functions + const setCookie = (name, value, days = 30) => { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};${expires};path=/;SameSite=Lax`; + }; + const getCookie = name => { + const nameEQ = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i].trim(); + if (cookie.indexOf(nameEQ) === 0) { + try { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } catch (e) { + console.error(`Error parsing cookie ${name}:`, e); + return []; + } + } + } + return []; + }; + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = getCookie('product_urls'); + } else if (source === 'marketing') { + urlsToCheck = getCookie('marketing_urls'); + } + if (!urlsToCheck.length) { + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); + return false; + } + const currentHost = window.location.hostname; + for (const allowedDomain of urlsToCheck) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + return false; + }; + const validateWebsite = async () => { + try { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': 'product_url', + 'step_id': 'setup_product_telemetry', + 'module_id': 'product_analytics' + }) + }); + const marketingResult = await marketingResponse.json(); + const productResult = await productResponse.json(); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); + console.log("Product API Response:", productResult); + let marketingUrls = []; + let productUrls = []; + if (typeof marketingResult === 'string') { + try { + const parsedResult = JSON.parse(marketingResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse marketing response:', parseError); + } + } else if (marketingResult && marketingResult.step_details && marketingResult.step_details.length > 0) { + const stepData = marketingResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing data:', parseError); + } + } + } + if (typeof productResult === 'string') { + try { + const parsedResult = JSON.parse(productResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse product response:', parseError); + } + } else if (productResult && productResult.step_details && productResult.step_details.length > 0) { + const stepData = productResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product data:', parseError); + } + } + } + setCookie('marketing_urls', marketingUrls); + setCookie('product_urls', productUrls); + console.log('Stored marketing URLs in cookies:', marketingUrls); + console.log('Stored product URLs in cookies:', productUrls); + const currentHost = window.location.hostname; + const isMarketingValid = marketingUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + const isProductValid = productUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + if (marketingUrls.length === 0 && productUrls.length === 0) { + console.error('ThriveStack Validation Error: Domain validation failed...'); + return false; + } + if (!isMarketingValid && !isProductValid) { + showValidationError(`Domain ${currentHost} is not authorized. Analytics tracking will be limited.`); + return false; + } + return true; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + const showValidationError = message => { + console.error('ThriveStack Validation Error:', message); + }; + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.src = 'https://thrivestack-temp-static-assets.s3.us-east-1.amazonaws.com/scripts/latest/thrivestack.js'; + if (API_KEY) { + script.setAttribute('api-key', API_KEY); + } + if (options.source) { + script.setAttribute('source', options.source); + } + if (options.trackClicks) { + script.setAttribute('track-clicks', 'true'); + } + if (options.trackForms) { + script.setAttribute('track-forms', 'true'); + } + if (options.respectDoNotTrack !== undefined) { + script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); + } + script.onload = () => { + if (window.ThriveStack) { + if (options.debug) { + window.ThriveStack.enableDebugMode(); + } + resolve(window.ThriveStack); + } else { + reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); + } + }; + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + document.head.appendChild(script); + }); + }; + const processQueue = () => { + if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { + return; + } + window._tsq.forEach(([method, args]) => { + if (typeof plugin[method] === 'function') { + try { + plugin[method](args); + } catch (err) { + console.error(`Failed to replay queued ${method}:`, err); + } + } + }); + window._tsq = []; + }; + const plugin = { + name: 'thrivestack', + config: { + ...pluginConfig, + options + }, + initialize: async ({ + config, + instance: analyticsInstance + }) => { + API_KEY = config.apiKey; + try { + await loadThriveStackScript(); + console.log('ThriveStack script loaded successfully'); + if (window.ThriveStack) { + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + const isValid = await validateWebsite(); + if (!isValid) { + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); + } + initComplete(analyticsInstance); + initCompleted = true; + processQueue(); + } catch (error) { + console.error('Failed to initialize ThriveStack plugin:', error); + throw error; + } + }, + // CORE-METHODS + + identify: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["identify", { + payload + }]); + return; + } + const { + userId, + traits = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + if (options.source === 'marketing' && !deviceId) { + const error = new Error('Identify call requires deviceId to be present'); + console.error('Failed to send identify event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const identifyPayload = [{ + user_id: userId || '', + traits: traits, + timestamp: options.timestamp || new Date().toISOString(), + context: { + group_id: options.groupId || options.group_id || window.ThriveStack.groupId || '', + device_id: deviceId, + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + window.ThriveStack.identify(identifyPayload).then(result => console.log('Identify sent successfully:', result)).catch(err => { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + }); + } catch (err) { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + track: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["track", { + payload + }]); + return; + } + const { + event, + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId && !deviceId) { + const error = new Error('Track event requires either userId or deviceId'); + console.error('Failed to send track event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const sessionId = window.ThriveStack.getSessionId() || ''; + const groupId = options.groupId || options.group_id || window.ThriveStack.groupId || ''; + const source = window.ThriveStack.getSource() || options.source || ''; + const eventPayload = [{ + event_name: event, + properties: properties, + user_id: userId || '', + context: { + group_id: groupId, + device_id: deviceId || '', + session_id: sessionId, + source: source + }, + timestamp: options.timestamp || new Date().toISOString() + }]; + window.ThriveStack.queueEvent(eventPayload); + } catch (err) { + console.error('Failed to send track event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + page: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["page", { + payload + }]); + return; + } + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); + return; + } + try { + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + const pastDate = new Date(0); + document.cookie = `thrivestack_user_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `thrivestack_group_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `product_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `marketing_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + console.log('ThriveStack data reset'); + if (next) next(payload); + return true; + } catch (err) { + console.error('Failed to reset ThriveStack data:', err); + if (next) next(payload); + return false; + } + }, + ready: payload => { + return initCompleted; + }, + storage: { + getItem: key => { + try { + return getCookie(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + setCookie(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + const pastDate = new Date(0); + document.cookie = `thrivestack_${key}=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } + try { + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); + } + return true; + } catch (err) { + console.error('Failed to set anonymous ID:', err); + return false; + } + }, + user: () => { + if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); + return; + } + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + methods: { + group: (groupId, traits = {}, options = {}, callback) => { + if (!window.ThriveStack) { + const error = new Error('ThriveStack not initialized'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); + showValidationError(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId) { + const error = new Error('Group identify requires userId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + if (!groupId) { + const error = new Error('Group identify requires groupId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const timestamp = options.timestamp || new Date().toISOString(); + const groupPayload = [{ + user_id: userId, + group_id: groupId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + group_type: traits.group_type, + device_id: window.ThriveStack.getDeviceId() || '', + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + return window.ThriveStack.group(groupPayload).then(result => { + if (callback && typeof callback === 'function') { + callback(null, result); + } + return result; + }).catch(err => { + showValidationError('Failed to send group event:'); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); + return false; + } + try { + window.ThriveStack.setConsent(category, enabled); + return true; + } catch (err) { + console.error('Failed to set consent:', err); + return false; + } + }, + getDeviceId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getDeviceId(); + } catch (err) { + console.error('Failed to get device ID:', err); + return null; + } + }, + getSessionId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSessionId(); + } catch (err) { + console.error('Failed to get session ID:', err); + return null; + } + }, + getUserId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.userId || null; + }, + getGroupId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.groupId || null; + }, + getSource: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSource(); + } catch (err) { + console.error('Failed to get source:', err); + return null; + } + }, + setSource: source => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.setSource(source); + return true; + } catch (err) { + console.error('Failed to set source:', err); + return false; + } + }, + getUtmParameters: () => { + if (!window.ThriveStack) { + return {}; + } + try { + return window.ThriveStack.getUtmParameters(); + } catch (err) { + console.error('Failed to get UTM parameters:', err); + return {}; + } + }, + // Page tracking methods + capturePageVisit: () => { + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + return false; + } + try { + window.ThriveStack.capturePageVisit(); + return true; + } catch (err) { + console.error('Failed to capture page visit:', err); + return false; + } + }, + // Debug methods + enableDebugMode: () => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.enableDebugMode(); + return true; + } catch (err) { + console.error('Failed to enable debug mode:', err); + return false; + } + }, + // Get ThriveStack instance (escape hatch) + getThriveStackInstance: () => { + return window.ThriveStack; + }, + // Validate website domain manually + validateWebsite: async () => { + return await validateWebsite(); + } + } + }; + return plugin; +} + +export { thriveStackPlugin as default }; diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.js b/packages/analytics-plugin-thrivestack/lib/index.browser.js new file mode 100644 index 00000000..7068c840 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -0,0 +1,691 @@ +'use strict'; + +function thriveStackPlugin(pluginConfig = {}) { + if (!pluginConfig.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + window._tsq = window._tsq || []; + window.ThriveStack = window.ThriveStack || null; + let initCompleted = false; + let API_KEY = pluginConfig.apiKey; + const options = { + respectDoNotTrack: pluginConfig.respectDoNotTrack !== false, + trackClicks: pluginConfig.trackClicks === true, + trackForms: pluginConfig.trackForms === true, + enableConsent: pluginConfig.enableConsent === true, + source: pluginConfig.source || '', + defaultConsent: pluginConfig.defaultConsent === true, + batchSize: pluginConfig.batchSize || 10, + batchInterval: pluginConfig.batchInterval || 2000, + ...pluginConfig.options + }; + const initComplete = i => { + initCompleted = true; + console.log('ThriveStack initialization completed'); + }; + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Cookie utility functions + const setCookie = (name, value, days = 30) => { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};${expires};path=/;SameSite=Lax`; + }; + const getCookie = name => { + const nameEQ = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i].trim(); + if (cookie.indexOf(nameEQ) === 0) { + try { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } catch (e) { + console.error(`Error parsing cookie ${name}:`, e); + return []; + } + } + } + return []; + }; + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = getCookie('product_urls'); + } else if (source === 'marketing') { + urlsToCheck = getCookie('marketing_urls'); + } + if (!urlsToCheck.length) { + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); + return false; + } + const currentHost = window.location.hostname; + for (const allowedDomain of urlsToCheck) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + return false; + }; + const validateWebsite = async () => { + try { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': 'product_url', + 'step_id': 'setup_product_telemetry', + 'module_id': 'product_analytics' + }) + }); + const marketingResult = await marketingResponse.json(); + const productResult = await productResponse.json(); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); + console.log("Product API Response:", productResult); + let marketingUrls = []; + let productUrls = []; + if (typeof marketingResult === 'string') { + try { + const parsedResult = JSON.parse(marketingResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse marketing response:', parseError); + } + } else if (marketingResult && marketingResult.step_details && marketingResult.step_details.length > 0) { + const stepData = marketingResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing data:', parseError); + } + } + } + if (typeof productResult === 'string') { + try { + const parsedResult = JSON.parse(productResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse product response:', parseError); + } + } else if (productResult && productResult.step_details && productResult.step_details.length > 0) { + const stepData = productResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product data:', parseError); + } + } + } + setCookie('marketing_urls', marketingUrls); + setCookie('product_urls', productUrls); + console.log('Stored marketing URLs in cookies:', marketingUrls); + console.log('Stored product URLs in cookies:', productUrls); + const currentHost = window.location.hostname; + const isMarketingValid = marketingUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + const isProductValid = productUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + if (marketingUrls.length === 0 && productUrls.length === 0) { + console.error('ThriveStack Validation Error: Domain validation failed...'); + return false; + } + if (!isMarketingValid && !isProductValid) { + showValidationError(`Domain ${currentHost} is not authorized. Analytics tracking will be limited.`); + return false; + } + return true; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + const showValidationError = message => { + console.error('ThriveStack Validation Error:', message); + }; + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.src = 'https://thrivestack-temp-static-assets.s3.us-east-1.amazonaws.com/scripts/latest/thrivestack.js'; + if (API_KEY) { + script.setAttribute('api-key', API_KEY); + } + if (options.source) { + script.setAttribute('source', options.source); + } + if (options.trackClicks) { + script.setAttribute('track-clicks', 'true'); + } + if (options.trackForms) { + script.setAttribute('track-forms', 'true'); + } + if (options.respectDoNotTrack !== undefined) { + script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); + } + script.onload = () => { + if (window.ThriveStack) { + if (options.debug) { + window.ThriveStack.enableDebugMode(); + } + resolve(window.ThriveStack); + } else { + reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); + } + }; + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + document.head.appendChild(script); + }); + }; + const processQueue = () => { + if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { + return; + } + window._tsq.forEach(([method, args]) => { + if (typeof plugin[method] === 'function') { + try { + plugin[method](args); + } catch (err) { + console.error(`Failed to replay queued ${method}:`, err); + } + } + }); + window._tsq = []; + }; + const plugin = { + name: 'thrivestack', + config: { + ...pluginConfig, + options + }, + initialize: async ({ + config, + instance: analyticsInstance + }) => { + API_KEY = config.apiKey; + try { + await loadThriveStackScript(); + console.log('ThriveStack script loaded successfully'); + if (window.ThriveStack) { + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + const isValid = await validateWebsite(); + if (!isValid) { + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); + } + initComplete(analyticsInstance); + initCompleted = true; + processQueue(); + } catch (error) { + console.error('Failed to initialize ThriveStack plugin:', error); + throw error; + } + }, + // CORE-METHODS + + identify: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["identify", { + payload + }]); + return; + } + const { + userId, + traits = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + if (options.source === 'marketing' && !deviceId) { + const error = new Error('Identify call requires deviceId to be present'); + console.error('Failed to send identify event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const identifyPayload = [{ + user_id: userId || '', + traits: traits, + timestamp: options.timestamp || new Date().toISOString(), + context: { + group_id: options.groupId || options.group_id || window.ThriveStack.groupId || '', + device_id: deviceId, + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + window.ThriveStack.identify(identifyPayload).then(result => console.log('Identify sent successfully:', result)).catch(err => { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + }); + } catch (err) { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + track: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["track", { + payload + }]); + return; + } + const { + event, + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId && !deviceId) { + const error = new Error('Track event requires either userId or deviceId'); + console.error('Failed to send track event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const sessionId = window.ThriveStack.getSessionId() || ''; + const groupId = options.groupId || options.group_id || window.ThriveStack.groupId || ''; + const source = window.ThriveStack.getSource() || options.source || ''; + const eventPayload = [{ + event_name: event, + properties: properties, + user_id: userId || '', + context: { + group_id: groupId, + device_id: deviceId || '', + session_id: sessionId, + source: source + }, + timestamp: options.timestamp || new Date().toISOString() + }]; + window.ThriveStack.queueEvent(eventPayload); + } catch (err) { + console.error('Failed to send track event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + page: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["page", { + payload + }]); + return; + } + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); + return; + } + try { + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + const pastDate = new Date(0); + document.cookie = `thrivestack_user_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `thrivestack_group_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `product_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `marketing_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + console.log('ThriveStack data reset'); + if (next) next(payload); + return true; + } catch (err) { + console.error('Failed to reset ThriveStack data:', err); + if (next) next(payload); + return false; + } + }, + ready: payload => { + return initCompleted; + }, + storage: { + getItem: key => { + try { + return getCookie(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + setCookie(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + const pastDate = new Date(0); + document.cookie = `thrivestack_${key}=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } + try { + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); + } + return true; + } catch (err) { + console.error('Failed to set anonymous ID:', err); + return false; + } + }, + user: () => { + if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); + return; + } + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + methods: { + group: (groupId, traits = {}, options = {}, callback) => { + if (!window.ThriveStack) { + const error = new Error('ThriveStack not initialized'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); + showValidationError(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId) { + const error = new Error('Group identify requires userId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + if (!groupId) { + const error = new Error('Group identify requires groupId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const timestamp = options.timestamp || new Date().toISOString(); + const groupPayload = [{ + user_id: userId, + group_id: groupId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + group_type: traits.group_type, + device_id: window.ThriveStack.getDeviceId() || '', + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + return window.ThriveStack.group(groupPayload).then(result => { + if (callback && typeof callback === 'function') { + callback(null, result); + } + return result; + }).catch(err => { + showValidationError('Failed to send group event:'); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); + return false; + } + try { + window.ThriveStack.setConsent(category, enabled); + return true; + } catch (err) { + console.error('Failed to set consent:', err); + return false; + } + }, + getDeviceId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getDeviceId(); + } catch (err) { + console.error('Failed to get device ID:', err); + return null; + } + }, + getSessionId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSessionId(); + } catch (err) { + console.error('Failed to get session ID:', err); + return null; + } + }, + getUserId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.userId || null; + }, + getGroupId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.groupId || null; + }, + getSource: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSource(); + } catch (err) { + console.error('Failed to get source:', err); + return null; + } + }, + setSource: source => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.setSource(source); + return true; + } catch (err) { + console.error('Failed to set source:', err); + return false; + } + }, + getUtmParameters: () => { + if (!window.ThriveStack) { + return {}; + } + try { + return window.ThriveStack.getUtmParameters(); + } catch (err) { + console.error('Failed to get UTM parameters:', err); + return {}; + } + }, + // Page tracking methods + capturePageVisit: () => { + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + return false; + } + try { + window.ThriveStack.capturePageVisit(); + return true; + } catch (err) { + console.error('Failed to capture page visit:', err); + return false; + } + }, + // Debug methods + enableDebugMode: () => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.enableDebugMode(); + return true; + } catch (err) { + console.error('Failed to enable debug mode:', err); + return false; + } + }, + // Get ThriveStack instance (escape hatch) + getThriveStackInstance: () => { + return window.ThriveStack; + }, + // Validate website domain manually + validateWebsite: async () => { + return await validateWebsite(); + } + } + }; + return plugin; +} + +module.exports = thriveStackPlugin; diff --git a/packages/analytics-plugin-thrivestack/lib/index.es.js b/packages/analytics-plugin-thrivestack/lib/index.es.js new file mode 100644 index 00000000..0d5b7243 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -0,0 +1,689 @@ +function thriveStackPlugin(pluginConfig = {}) { + if (!pluginConfig.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + window._tsq = window._tsq || []; + window.ThriveStack = window.ThriveStack || null; + let initCompleted = false; + let API_KEY = pluginConfig.apiKey; + const options = { + respectDoNotTrack: pluginConfig.respectDoNotTrack !== false, + trackClicks: pluginConfig.trackClicks === true, + trackForms: pluginConfig.trackForms === true, + enableConsent: pluginConfig.enableConsent === true, + source: pluginConfig.source || '', + defaultConsent: pluginConfig.defaultConsent === true, + batchSize: pluginConfig.batchSize || 10, + batchInterval: pluginConfig.batchInterval || 2000, + ...pluginConfig.options + }; + const initComplete = i => { + initCompleted = true; + console.log('ThriveStack initialization completed'); + }; + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Cookie utility functions + const setCookie = (name, value, days = 30) => { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};${expires};path=/;SameSite=Lax`; + }; + const getCookie = name => { + const nameEQ = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i].trim(); + if (cookie.indexOf(nameEQ) === 0) { + try { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } catch (e) { + console.error(`Error parsing cookie ${name}:`, e); + return []; + } + } + } + return []; + }; + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = getCookie('product_urls'); + } else if (source === 'marketing') { + urlsToCheck = getCookie('marketing_urls'); + } + if (!urlsToCheck.length) { + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); + return false; + } + const currentHost = window.location.hostname; + for (const allowedDomain of urlsToCheck) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + return false; + }; + const validateWebsite = async () => { + try { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': 'product_url', + 'step_id': 'setup_product_telemetry', + 'module_id': 'product_analytics' + }) + }); + const marketingResult = await marketingResponse.json(); + const productResult = await productResponse.json(); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); + console.log("Product API Response:", productResult); + let marketingUrls = []; + let productUrls = []; + if (typeof marketingResult === 'string') { + try { + const parsedResult = JSON.parse(marketingResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse marketing response:', parseError); + } + } else if (marketingResult && marketingResult.step_details && marketingResult.step_details.length > 0) { + const stepData = marketingResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing data:', parseError); + } + } + } + if (typeof productResult === 'string') { + try { + const parsedResult = JSON.parse(productResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse product response:', parseError); + } + } else if (productResult && productResult.step_details && productResult.step_details.length > 0) { + const stepData = productResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product data:', parseError); + } + } + } + setCookie('marketing_urls', marketingUrls); + setCookie('product_urls', productUrls); + console.log('Stored marketing URLs in cookies:', marketingUrls); + console.log('Stored product URLs in cookies:', productUrls); + const currentHost = window.location.hostname; + const isMarketingValid = marketingUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + const isProductValid = productUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + if (marketingUrls.length === 0 && productUrls.length === 0) { + console.error('ThriveStack Validation Error: Domain validation failed...'); + return false; + } + if (!isMarketingValid && !isProductValid) { + showValidationError(`Domain ${currentHost} is not authorized. Analytics tracking will be limited.`); + return false; + } + return true; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + const showValidationError = message => { + console.error('ThriveStack Validation Error:', message); + }; + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.src = 'https://thrivestack-temp-static-assets.s3.us-east-1.amazonaws.com/scripts/latest/thrivestack.js'; + if (API_KEY) { + script.setAttribute('api-key', API_KEY); + } + if (options.source) { + script.setAttribute('source', options.source); + } + if (options.trackClicks) { + script.setAttribute('track-clicks', 'true'); + } + if (options.trackForms) { + script.setAttribute('track-forms', 'true'); + } + if (options.respectDoNotTrack !== undefined) { + script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); + } + script.onload = () => { + if (window.ThriveStack) { + if (options.debug) { + window.ThriveStack.enableDebugMode(); + } + resolve(window.ThriveStack); + } else { + reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); + } + }; + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + document.head.appendChild(script); + }); + }; + const processQueue = () => { + if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { + return; + } + window._tsq.forEach(([method, args]) => { + if (typeof plugin[method] === 'function') { + try { + plugin[method](args); + } catch (err) { + console.error(`Failed to replay queued ${method}:`, err); + } + } + }); + window._tsq = []; + }; + const plugin = { + name: 'thrivestack', + config: { + ...pluginConfig, + options + }, + initialize: async ({ + config, + instance: analyticsInstance + }) => { + API_KEY = config.apiKey; + try { + await loadThriveStackScript(); + console.log('ThriveStack script loaded successfully'); + if (window.ThriveStack) { + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + const isValid = await validateWebsite(); + if (!isValid) { + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); + } + initComplete(analyticsInstance); + initCompleted = true; + processQueue(); + } catch (error) { + console.error('Failed to initialize ThriveStack plugin:', error); + throw error; + } + }, + // CORE-METHODS + + identify: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["identify", { + payload + }]); + return; + } + const { + userId, + traits = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + if (options.source === 'marketing' && !deviceId) { + const error = new Error('Identify call requires deviceId to be present'); + console.error('Failed to send identify event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const identifyPayload = [{ + user_id: userId || '', + traits: traits, + timestamp: options.timestamp || new Date().toISOString(), + context: { + group_id: options.groupId || options.group_id || window.ThriveStack.groupId || '', + device_id: deviceId, + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + window.ThriveStack.identify(identifyPayload).then(result => console.log('Identify sent successfully:', result)).catch(err => { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + }); + } catch (err) { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + track: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["track", { + payload + }]); + return; + } + const { + event, + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId && !deviceId) { + const error = new Error('Track event requires either userId or deviceId'); + console.error('Failed to send track event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const sessionId = window.ThriveStack.getSessionId() || ''; + const groupId = options.groupId || options.group_id || window.ThriveStack.groupId || ''; + const source = window.ThriveStack.getSource() || options.source || ''; + const eventPayload = [{ + event_name: event, + properties: properties, + user_id: userId || '', + context: { + group_id: groupId, + device_id: deviceId || '', + session_id: sessionId, + source: source + }, + timestamp: options.timestamp || new Date().toISOString() + }]; + window.ThriveStack.queueEvent(eventPayload); + } catch (err) { + console.error('Failed to send track event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + page: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["page", { + payload + }]); + return; + } + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); + return; + } + try { + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + const pastDate = new Date(0); + document.cookie = `thrivestack_user_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `thrivestack_group_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `product_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `marketing_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + console.log('ThriveStack data reset'); + if (next) next(payload); + return true; + } catch (err) { + console.error('Failed to reset ThriveStack data:', err); + if (next) next(payload); + return false; + } + }, + ready: payload => { + return initCompleted; + }, + storage: { + getItem: key => { + try { + return getCookie(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + setCookie(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + const pastDate = new Date(0); + document.cookie = `thrivestack_${key}=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } + try { + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); + } + return true; + } catch (err) { + console.error('Failed to set anonymous ID:', err); + return false; + } + }, + user: () => { + if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); + return; + } + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + methods: { + group: (groupId, traits = {}, options = {}, callback) => { + if (!window.ThriveStack) { + const error = new Error('ThriveStack not initialized'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); + showValidationError(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId) { + const error = new Error('Group identify requires userId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + if (!groupId) { + const error = new Error('Group identify requires groupId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const timestamp = options.timestamp || new Date().toISOString(); + const groupPayload = [{ + user_id: userId, + group_id: groupId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + group_type: traits.group_type, + device_id: window.ThriveStack.getDeviceId() || '', + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + return window.ThriveStack.group(groupPayload).then(result => { + if (callback && typeof callback === 'function') { + callback(null, result); + } + return result; + }).catch(err => { + showValidationError('Failed to send group event:'); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); + return false; + } + try { + window.ThriveStack.setConsent(category, enabled); + return true; + } catch (err) { + console.error('Failed to set consent:', err); + return false; + } + }, + getDeviceId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getDeviceId(); + } catch (err) { + console.error('Failed to get device ID:', err); + return null; + } + }, + getSessionId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSessionId(); + } catch (err) { + console.error('Failed to get session ID:', err); + return null; + } + }, + getUserId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.userId || null; + }, + getGroupId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.groupId || null; + }, + getSource: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSource(); + } catch (err) { + console.error('Failed to get source:', err); + return null; + } + }, + setSource: source => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.setSource(source); + return true; + } catch (err) { + console.error('Failed to set source:', err); + return false; + } + }, + getUtmParameters: () => { + if (!window.ThriveStack) { + return {}; + } + try { + return window.ThriveStack.getUtmParameters(); + } catch (err) { + console.error('Failed to get UTM parameters:', err); + return {}; + } + }, + // Page tracking methods + capturePageVisit: () => { + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + return false; + } + try { + window.ThriveStack.capturePageVisit(); + return true; + } catch (err) { + console.error('Failed to capture page visit:', err); + return false; + } + }, + // Debug methods + enableDebugMode: () => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.enableDebugMode(); + return true; + } catch (err) { + console.error('Failed to enable debug mode:', err); + return false; + } + }, + // Get ThriveStack instance (escape hatch) + getThriveStackInstance: () => { + return window.ThriveStack; + }, + // Validate website domain manually + validateWebsite: async () => { + return await validateWebsite(); + } + } + }; + return plugin; +} + +export { thriveStackPlugin as default }; diff --git a/packages/analytics-plugin-thrivestack/lib/index.js b/packages/analytics-plugin-thrivestack/lib/index.js new file mode 100644 index 00000000..7068c840 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -0,0 +1,691 @@ +'use strict'; + +function thriveStackPlugin(pluginConfig = {}) { + if (!pluginConfig.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + window._tsq = window._tsq || []; + window.ThriveStack = window.ThriveStack || null; + let initCompleted = false; + let API_KEY = pluginConfig.apiKey; + const options = { + respectDoNotTrack: pluginConfig.respectDoNotTrack !== false, + trackClicks: pluginConfig.trackClicks === true, + trackForms: pluginConfig.trackForms === true, + enableConsent: pluginConfig.enableConsent === true, + source: pluginConfig.source || '', + defaultConsent: pluginConfig.defaultConsent === true, + batchSize: pluginConfig.batchSize || 10, + batchInterval: pluginConfig.batchInterval || 2000, + ...pluginConfig.options + }; + const initComplete = i => { + initCompleted = true; + console.log('ThriveStack initialization completed'); + }; + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Cookie utility functions + const setCookie = (name, value, days = 30) => { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};${expires};path=/;SameSite=Lax`; + }; + const getCookie = name => { + const nameEQ = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i].trim(); + if (cookie.indexOf(nameEQ) === 0) { + try { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } catch (e) { + console.error(`Error parsing cookie ${name}:`, e); + return []; + } + } + } + return []; + }; + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = getCookie('product_urls'); + } else if (source === 'marketing') { + urlsToCheck = getCookie('marketing_urls'); + } + if (!urlsToCheck.length) { + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); + return false; + } + const currentHost = window.location.hostname; + for (const allowedDomain of urlsToCheck) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + return false; + }; + const validateWebsite = async () => { + try { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': 'product_url', + 'step_id': 'setup_product_telemetry', + 'module_id': 'product_analytics' + }) + }); + const marketingResult = await marketingResponse.json(); + const productResult = await productResponse.json(); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); + console.log("Product API Response:", productResult); + let marketingUrls = []; + let productUrls = []; + if (typeof marketingResult === 'string') { + try { + const parsedResult = JSON.parse(marketingResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse marketing response:', parseError); + } + } else if (marketingResult && marketingResult.step_details && marketingResult.step_details.length > 0) { + const stepData = marketingResult.step_details.find(step => step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing data:', parseError); + } + } + } + if (typeof productResult === 'string') { + try { + const parsedResult = JSON.parse(productResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse product response:', parseError); + } + } else if (productResult && productResult.step_details && productResult.step_details.length > 0) { + const stepData = productResult.step_details.find(step => step.module_id === 'product_analytics' && step.step_id === 'setup_product_telemetry' && step.sub_step_id === 'product_url'); + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product data:', parseError); + } + } + } + setCookie('marketing_urls', marketingUrls); + setCookie('product_urls', productUrls); + console.log('Stored marketing URLs in cookies:', marketingUrls); + console.log('Stored product URLs in cookies:', productUrls); + const currentHost = window.location.hostname; + const isMarketingValid = marketingUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + const isProductValid = productUrls.some(url => currentHost.includes(url) || url.includes(currentHost)); + if (marketingUrls.length === 0 && productUrls.length === 0) { + console.error('ThriveStack Validation Error: Domain validation failed...'); + return false; + } + if (!isMarketingValid && !isProductValid) { + showValidationError(`Domain ${currentHost} is not authorized. Analytics tracking will be limited.`); + return false; + } + return true; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + const showValidationError = message => { + console.error('ThriveStack Validation Error:', message); + }; + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.src = 'https://thrivestack-temp-static-assets.s3.us-east-1.amazonaws.com/scripts/latest/thrivestack.js'; + if (API_KEY) { + script.setAttribute('api-key', API_KEY); + } + if (options.source) { + script.setAttribute('source', options.source); + } + if (options.trackClicks) { + script.setAttribute('track-clicks', 'true'); + } + if (options.trackForms) { + script.setAttribute('track-forms', 'true'); + } + if (options.respectDoNotTrack !== undefined) { + script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); + } + script.onload = () => { + if (window.ThriveStack) { + if (options.debug) { + window.ThriveStack.enableDebugMode(); + } + resolve(window.ThriveStack); + } else { + reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); + } + }; + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + document.head.appendChild(script); + }); + }; + const processQueue = () => { + if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { + return; + } + window._tsq.forEach(([method, args]) => { + if (typeof plugin[method] === 'function') { + try { + plugin[method](args); + } catch (err) { + console.error(`Failed to replay queued ${method}:`, err); + } + } + }); + window._tsq = []; + }; + const plugin = { + name: 'thrivestack', + config: { + ...pluginConfig, + options + }, + initialize: async ({ + config, + instance: analyticsInstance + }) => { + API_KEY = config.apiKey; + try { + await loadThriveStackScript(); + console.log('ThriveStack script loaded successfully'); + if (window.ThriveStack) { + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + const isValid = await validateWebsite(); + if (!isValid) { + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); + } + initComplete(analyticsInstance); + initCompleted = true; + processQueue(); + } catch (error) { + console.error('Failed to initialize ThriveStack plugin:', error); + throw error; + } + }, + // CORE-METHODS + + identify: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["identify", { + payload + }]); + return; + } + const { + userId, + traits = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + if (options.source === 'marketing' && !deviceId) { + const error = new Error('Identify call requires deviceId to be present'); + console.error('Failed to send identify event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const identifyPayload = [{ + user_id: userId || '', + traits: traits, + timestamp: options.timestamp || new Date().toISOString(), + context: { + group_id: options.groupId || options.group_id || window.ThriveStack.groupId || '', + device_id: deviceId, + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + window.ThriveStack.identify(identifyPayload).then(result => console.log('Identify sent successfully:', result)).catch(err => { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + }); + } catch (err) { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + track: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["track", { + payload + }]); + return; + } + const { + event, + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + try { + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId && !deviceId) { + const error = new Error('Track event requires either userId or deviceId'); + console.error('Failed to send track event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + const sessionId = window.ThriveStack.getSessionId() || ''; + const groupId = options.groupId || options.group_id || window.ThriveStack.groupId || ''; + const source = window.ThriveStack.getSource() || options.source || ''; + const eventPayload = [{ + event_name: event, + properties: properties, + user_id: userId || '', + context: { + group_id: groupId, + device_id: deviceId || '', + session_id: sessionId, + source: source + }, + timestamp: options.timestamp || new Date().toISOString() + }]; + window.ThriveStack.queueEvent(eventPayload); + } catch (err) { + console.error('Failed to send track event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + page: ({ + payload + }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["page", { + payload + }]); + return; + } + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); + return; + } + try { + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + const pastDate = new Date(0); + document.cookie = `thrivestack_user_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `thrivestack_group_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `product_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `marketing_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + console.log('ThriveStack data reset'); + if (next) next(payload); + return true; + } catch (err) { + console.error('Failed to reset ThriveStack data:', err); + if (next) next(payload); + return false; + } + }, + ready: payload => { + return initCompleted; + }, + storage: { + getItem: key => { + try { + return getCookie(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + setCookie(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + const pastDate = new Date(0); + document.cookie = `thrivestack_${key}=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } + try { + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); + } + return true; + } catch (err) { + console.error('Failed to set anonymous ID:', err); + return false; + } + }, + user: () => { + if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); + return; + } + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + methods: { + group: (groupId, traits = {}, options = {}, callback) => { + if (!window.ThriveStack) { + const error = new Error('ThriveStack not initialized'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); + showValidationError(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const userId = options.userId || options.user_id || window.ThriveStack.userId; + if (options.source === 'marketing' && !userId) { + const error = new Error('Group identify requires userId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + if (!groupId) { + const error = new Error('Group identify requires groupId to be present'); + showValidationError('Group identify failed:'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + const timestamp = options.timestamp || new Date().toISOString(); + const groupPayload = [{ + user_id: userId, + group_id: groupId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + group_type: traits.group_type, + device_id: window.ThriveStack.getDeviceId() || '', + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + return window.ThriveStack.group(groupPayload).then(result => { + if (callback && typeof callback === 'function') { + callback(null, result); + } + return result; + }).catch(err => { + showValidationError('Failed to send group event:'); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); + return false; + } + try { + window.ThriveStack.setConsent(category, enabled); + return true; + } catch (err) { + console.error('Failed to set consent:', err); + return false; + } + }, + getDeviceId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getDeviceId(); + } catch (err) { + console.error('Failed to get device ID:', err); + return null; + } + }, + getSessionId: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSessionId(); + } catch (err) { + console.error('Failed to get session ID:', err); + return null; + } + }, + getUserId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.userId || null; + }, + getGroupId: () => { + if (!window.ThriveStack) { + return null; + } + return window.ThriveStack.groupId || null; + }, + getSource: () => { + if (!window.ThriveStack) { + return null; + } + try { + return window.ThriveStack.getSource(); + } catch (err) { + console.error('Failed to get source:', err); + return null; + } + }, + setSource: source => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.setSource(source); + return true; + } catch (err) { + console.error('Failed to set source:', err); + return false; + } + }, + getUtmParameters: () => { + if (!window.ThriveStack) { + return {}; + } + try { + return window.ThriveStack.getUtmParameters(); + } catch (err) { + console.error('Failed to get UTM parameters:', err); + return {}; + } + }, + // Page tracking methods + capturePageVisit: () => { + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + return false; + } + try { + window.ThriveStack.capturePageVisit(); + return true; + } catch (err) { + console.error('Failed to capture page visit:', err); + return false; + } + }, + // Debug methods + enableDebugMode: () => { + if (!window.ThriveStack) { + return false; + } + try { + window.ThriveStack.enableDebugMode(); + return true; + } catch (err) { + console.error('Failed to enable debug mode:', err); + return false; + } + }, + // Get ThriveStack instance (escape hatch) + getThriveStackInstance: () => { + return window.ThriveStack; + }, + // Validate website domain manually + validateWebsite: async () => { + return await validateWebsite(); + } + } + }; + return plugin; +} + +module.exports = thriveStackPlugin; diff --git a/packages/analytics-plugin-thrivestack/package.json b/packages/analytics-plugin-thrivestack/package.json new file mode 100644 index 00000000..9d84a9d9 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/package.json @@ -0,0 +1,35 @@ +{ + "name": "@analytics/thrivestack", + "version": "0.1.0", + "description": "ThriveStack integration for 'analytics' module", + "keywords": [ + "analytics", + "analytics-plugin", + "thrivestack" + ], + "author": "aniketd", + "license": "MIT", + "main": "lib/index.js", + "module": "lib/index.es.js", + "browser": { + "./lib/index.js": "./lib/index.browser.js", + "./lib/index.es.js": "./lib/index.browser.es.js" + }, + "files": [ + "dist", + "lib", + "README.md" + ], + "homepage": "https://github.com/DavidWells/analytics/tree/master/packages/analytics-plugin-thrivestack", + "repository": { + "type": "git", + "url": "git+https://github.com/DavidWells/analytics.git" + }, + "scripts": { + "build": "node ../../scripts/build/index.js", + "watch": "node ../../scripts/build/watch.js", + "release:patch": "npm version patch && npm publish", + "release:minor": "npm version minor && npm publish", + "release:major": "npm version major && npm publish" + } +} diff --git a/packages/analytics-plugin-thrivestack/src/api.js b/packages/analytics-plugin-thrivestack/src/api.js new file mode 100644 index 00000000..badeeeca --- /dev/null +++ b/packages/analytics-plugin-thrivestack/src/api.js @@ -0,0 +1,121 @@ +/** + * ThriveStack API client for making server-side requests + */ + +/** + * Make a request to the ThriveStack API + * @param {string} endpoint - API endpoint + * @param {object} data - Data to send to the API + * @param {object} options - Request options + * @param {string} apiKey - ThriveStack API key + * @returns {Promise} - API response + */ +async function makeRequest(endpoint, data = {}, options = {}, apiKey) { + if (!apiKey) { + throw new Error('API key is required for ThriveStack API requests') + } + + const baseUrl = options.baseUrl || 'https://api.thrivestack.io' + const url = `${baseUrl}/${endpoint}` + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + ...options.headers + }, + body: JSON.stringify(data) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`ThriveStack API error (${response.status}): ${errorText}`) + } + + return await response.json() + } catch (error) { + console.error('ThriveStack API request failed:', error) + throw error + } + } + + /** + * Get events from ThriveStack + * @param {object} options - Query options + * @param {string} apiKey - ThriveStack API key + * @returns {Promise} - API response with events + */ + export async function getEvents(options = {}, apiKey) { + const { startDate, endDate, limit = 100, offset = 0, ...restOptions } = options + + const queryParams = { + startDate, + endDate, + limit, + offset, + ...restOptions + } + + return makeRequest('events', queryParams, {}, apiKey) + } + + /** + * Get page visits from ThriveStack + * @param {object} options - Query options + * @param {string} apiKey - ThriveStack API key + * @returns {Promise} - API response with page visits + */ + export async function getPageVisits(options = {}, apiKey) { + const { startDate, endDate, limit = 100, offset = 0, ...restOptions } = options + + const queryParams = { + startDate, + endDate, + limit, + offset, + ...restOptions + } + + return makeRequest('page-visits', queryParams, {}, apiKey) + } + + /** + * Get user data from ThriveStack + * @param {string} userId - User ID + * @param {object} options - Query options + * @param {string} apiKey - ThriveStack API key + * @returns {Promise} - API response with user data + */ + export async function getUserData(userId, options = {}, apiKey) { + if (!userId) { + throw new Error('User ID is required to get user data') + } + + return makeRequest(`users/${userId}`, {}, options, apiKey) + } + + /** + * Get dashboard statistics from ThriveStack + * @param {object} options - Query options + * @param {string} apiKey - ThriveStack API key + * @returns {Promise} - API response with dashboard statistics + */ + export async function getDashboardStats(options = {}, apiKey) { + const { period = '7d', ...restOptions } = options + + const queryParams = { + period, + ...restOptions + } + + return makeRequest('dashboard/stats', queryParams, {}, apiKey) + } + + export default { + getEvents, + getPageVisits, + getUserData, + getDashboardStats + } \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/src/browser.js b/packages/analytics-plugin-thrivestack/src/browser.js new file mode 100644 index 00000000..eb3f78b9 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/src/browser.js @@ -0,0 +1,7 @@ +/** + * Browser implementation for ThriveStack plugin + */ + +import thriveStackPlugin from '.' + +export default thriveStackPlugin \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/src/index.js b/packages/analytics-plugin-thrivestack/src/index.js new file mode 100644 index 00000000..c4302d3e --- /dev/null +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -0,0 +1,811 @@ +function thriveStackPlugin(pluginConfig = {}) { + if (!pluginConfig.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + + window._tsq = window._tsq || []; + window.ThriveStack = window.ThriveStack || null; + + let instance = null; + let initCompleted = false; + let API_KEY = pluginConfig.apiKey; + const options = { + respectDoNotTrack: pluginConfig.respectDoNotTrack !== false, + trackClicks: pluginConfig.trackClicks === true, + trackForms: pluginConfig.trackForms === true, + enableConsent: pluginConfig.enableConsent === true, + source: pluginConfig.source || '', + defaultConsent: pluginConfig.defaultConsent === true, + batchSize: pluginConfig.batchSize || 10, + batchInterval: pluginConfig.batchInterval || 2000, + ...pluginConfig.options + }; + + const initComplete = (i) => { + instance = i; + initCompleted = true; + console.log('ThriveStack initialization completed'); + }; + + const normalizeUrl = (url) => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Cookie utility functions + const setCookie = (name, value, days = 30) => { + const date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};${expires};path=/;SameSite=Lax`; + }; + + const getCookie = (name) => { + const nameEQ = `${name}=`; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i].trim(); + if (cookie.indexOf(nameEQ) === 0) { + try { + return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length))); + } catch (e) { + console.error(`Error parsing cookie ${name}:`, e); + return []; + } + } + } + return []; + }; + + const isValidDomain = (source = '') => { + let urlsToCheck = []; + + if (source === 'product') { + urlsToCheck = getCookie('product_urls'); + } else if (source === 'marketing') { + urlsToCheck = getCookie('marketing_urls'); + } + + if (!urlsToCheck.length) { + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); + return false; + } + + const currentHost = window.location.hostname; + + for (const allowedDomain of urlsToCheck) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + return false; + }; + + const validateWebsite = async () => { + try { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': pluginConfig.apiKey + }, + body: JSON.stringify({ + 'sub_step_id': 'product_url', + 'step_id': 'setup_product_telemetry', + 'module_id': 'product_analytics' + }) + }); + + const marketingResult = await marketingResponse.json(); + const productResult = await productResponse.json(); + + console.log("Marketing API Response:", marketingResult, pluginConfig.source); + console.log("Product API Response:", productResult); + + let marketingUrls = []; + let productUrls = []; + + if (typeof marketingResult === 'string') { + try { + const parsedResult = JSON.parse(marketingResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => + step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse marketing response:', parseError); + } + } else if (marketingResult && marketingResult.step_details && marketingResult.step_details.length > 0) { + const stepData = marketingResult.step_details.find(step => + step.module_id === 'marketing_attribution' && step.step_id === 'name_your_website'); + + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + marketingUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing data:', parseError); + } + } + } + + if (typeof productResult === 'string') { + try { + const parsedResult = JSON.parse(productResult); + if (parsedResult.step_details && parsedResult.step_details.length > 0) { + const stepData = parsedResult.step_details.find(step => + step.module_id === 'product_analytics' && + step.step_id === 'setup_product_telemetry' && + step.sub_step_id === 'product_url'); + + if (stepData && stepData.data) { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } + } + } catch (parseError) { + console.error('Failed to parse product response:', parseError); + } + } else if (productResult && productResult.step_details && productResult.step_details.length > 0) { + const stepData = productResult.step_details.find(step => + step.module_id === 'product_analytics' && + step.step_id === 'setup_product_telemetry' && + step.sub_step_id === 'product_url'); + + if (stepData && stepData.data) { + try { + const dataObj = JSON.parse(stepData.data); + if (dataObj.website_urls && Array.isArray(dataObj.website_urls)) { + productUrls = dataObj.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product data:', parseError); + } + } + } + + setCookie('marketing_urls', marketingUrls); + setCookie('product_urls', productUrls); + + console.log('Stored marketing URLs in cookies:', marketingUrls); + console.log('Stored product URLs in cookies:', productUrls); + + const currentHost = window.location.hostname; + + const isMarketingValid = marketingUrls.some(url => + currentHost.includes(url) || url.includes(currentHost) + ); + + const isProductValid = productUrls.some(url => + currentHost.includes(url) || url.includes(currentHost) + ); + + if (marketingUrls.length === 0 && productUrls.length === 0) { + console.error('ThriveStack Validation Error: Domain validation failed...'); + return false; + } + + if (!isMarketingValid && !isProductValid) { + + showValidationError(`Domain ${currentHost} is not authorized. Analytics tracking will be limited.`); + return false; + } + + return true; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + + const showValidationError = (message) => { + console.error('ThriveStack Validation Error:', message); + }; + + + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.src = 'https://thrivestack-temp-static-assets.s3.us-east-1.amazonaws.com/scripts/latest/thrivestack.js' + + if (API_KEY) { + script.setAttribute('api-key', API_KEY); + } + + if (options.source) { + script.setAttribute('source', options.source); + } + + if (options.trackClicks) { + script.setAttribute('track-clicks', 'true'); + } + + if (options.trackForms) { + script.setAttribute('track-forms', 'true'); + } + + if (options.respectDoNotTrack !== undefined) { + script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); + } + + script.onload = () => { + if (window.ThriveStack) { + if (options.debug) { + window.ThriveStack.enableDebugMode(); + } + resolve(window.ThriveStack); + } else { + reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); + } + }; + + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + + document.head.appendChild(script); + }); + }; + + const processQueue = () => { + if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { + return; + } + + window._tsq.forEach(([method, args]) => { + if (typeof plugin[method] === 'function') { + try { + plugin[method](args); + } catch (err) { + console.error(`Failed to replay queued ${method}:`, err); + } + } + }); + + window._tsq = []; + }; + + const plugin = { + name: 'thrivestack', + config: { + ...pluginConfig, + options + }, + + initialize: async ({ config, instance: analyticsInstance }) => { + + API_KEY = config.apiKey; + + try { + await loadThriveStackScript(); + console.log('ThriveStack script loaded successfully'); + + if (window.ThriveStack) { + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + + console.log('ThriveStack plugin initialized'); + } + + const isValid = await validateWebsite(); + + if (!isValid) { + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); + } + + initComplete(analyticsInstance); + initCompleted = true; + + processQueue(); + } catch (error) { + console.error('Failed to initialize ThriveStack plugin:', error); + throw error; + } + }, + + // CORE-METHODS + + identify: ({ payload }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["identify", { payload }]); + return; + } + const { userId, traits = {}, options = {} } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + + try { + const deviceId = window.ThriveStack.getDeviceId(); + + if (options.source === 'marketing' && !deviceId) { + const error = new Error('Identify call requires deviceId to be present'); + console.error('Failed to send identify event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + + const identifyPayload = [{ + user_id: userId || '', + traits: traits, + timestamp: options.timestamp || new Date().toISOString(), + context: { + group_id: options.groupId || options.group_id || window.ThriveStack.groupId || '', + device_id: deviceId, + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + + window.ThriveStack.identify(identifyPayload) + .then(result => console.log('Identify sent successfully:', result)) + .catch(err => { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + }); + } catch (err) { + console.error('Failed to send identify event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + + track: ({ payload }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["track", { payload }]); + return; + } + + const { event, properties = {}, options = {} } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); + return; + } + + try { + + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + + if (options.source === 'marketing' && !userId && !deviceId) { + const error = new Error('Track event requires either userId or deviceId'); + console.error('Failed to send track event:', error); + if (options.callback && typeof options.callback === 'function') { + options.callback(error); + } + return; + } + + const sessionId = window.ThriveStack.getSessionId() || ''; + const groupId = options.groupId || options.group_id || window.ThriveStack.groupId || ''; + const source = window.ThriveStack.getSource() || options.source || ''; + + const eventPayload = [{ + event_name: event, + properties: properties, + user_id: userId || '', + context: { + group_id: groupId, + device_id: deviceId || '', + session_id: sessionId, + source: source + }, + timestamp: options.timestamp || new Date().toISOString() + }]; + + window.ThriveStack.queueEvent(eventPayload); + } catch (err) { + console.error('Failed to send track event:', err); + if (options.callback && typeof options.callback === 'function') { + options.callback(err); + } + } + }, + + page: ({ payload }) => { + if (!initCompleted || !window.ThriveStack) { + window._tsq.push(["page", { payload }]); + return; + } + + const { properties = {}, options = {} } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); + return; + } + + try { + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + + try { + window.ThriveStack.setUserId('') + window.ThriveStack.setGroupId('') + + const pastDate = new Date(0); + document.cookie = `thrivestack_user_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `thrivestack_group_id=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `product_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + document.cookie = `marketing_urls=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + + console.log('ThriveStack data reset'); + if (next) next(payload); + return true; + } catch (err) { + console.error('Failed to reset ThriveStack data:', err); + if (next) next(payload); + return false; + } + }, + + ready: (payload) => { + return initCompleted; + }, + + storage: { + getItem: (key) => { + try { + return getCookie(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + + setItem: (key, value) => { + try { + setCookie(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + + removeItem: (key) => { + try { + const pastDate = new Date(0); + document.cookie = `thrivestack_${key}=;expires=${pastDate.toUTCString()};path=/;SameSite=Lax`; + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + + setAnonymousId: (anonymousId) => { + if (!window.ThriveStack) return false; + + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } + + try { + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); + } + return true; + } catch (err) { + console.error('Failed to set anonymous ID:', err); + return false; + } + }, + + user: () => { + if (!window.ThriveStack) return null; + + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); + return; + } + + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + + methods: { + group: (groupId, traits = {}, options = {}, callback) => { + if (!window.ThriveStack) { + const error = new Error('ThriveStack not initialized'); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); + showValidationError(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + if (options.source === 'marketing' && !userId) { + const error = new Error('Group identify requires userId to be present'); + showValidationError('Group identify failed:', error); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + if (!groupId) { + const error = new Error('Group identify requires groupId to be present'); + showValidationError('Group identify failed:', error); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + const timestamp = options.timestamp || new Date().toISOString(); + + const groupPayload = [{ + user_id: userId, + group_id: groupId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + group_type: traits.group_type, + device_id: window.ThriveStack.getDeviceId() || '', + session_id: window.ThriveStack.getSessionId() || '', + source: window.ThriveStack.getSource() || options.source || '' + } + }]; + + return window.ThriveStack.group(groupPayload) + .then(result => { + if (callback && typeof callback === 'function') { + callback(null, result); + } + return result; + }) + .catch(err => { + showValidationError('Failed to send group event:', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + + setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } + + if (config.apiKey) { + API_KEY = config.apiKey; + } + + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + + return true; + }, + + setConsent: (category, enabled) => { + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); + return false; + } + + try { + window.ThriveStack.setConsent(category, enabled); + return true; + } catch (err) { + console.error('Failed to set consent:', err); + return false; + } + }, + + getDeviceId: () => { + if (!window.ThriveStack) { + return null; + } + + try { + return window.ThriveStack.getDeviceId(); + } catch (err) { + console.error('Failed to get device ID:', err); + return null; + } + }, + + getSessionId: () => { + if (!window.ThriveStack) { + return null; + } + + try { + return window.ThriveStack.getSessionId(); + } catch (err) { + console.error('Failed to get session ID:', err); + return null; + } + }, + + getUserId: () => { + if (!window.ThriveStack) { + return null; + } + + return window.ThriveStack.userId || null; + }, + + getGroupId: () => { + if (!window.ThriveStack) { + return null; + } + + return window.ThriveStack.groupId || null; + }, + + getSource: () => { + if (!window.ThriveStack) { + return null; + } + + try { + return window.ThriveStack.getSource(); + } catch (err) { + console.error('Failed to get source:', err); + return null; + } + }, + + setSource: (source) => { + if (!window.ThriveStack) { + return false; + } + + try { + window.ThriveStack.setSource(source); + return true; + } catch (err) { + console.error('Failed to set source:', err); + return false; + } + }, + + getUtmParameters: () => { + if (!window.ThriveStack) { + return {}; + } + + try { + return window.ThriveStack.getUtmParameters(); + } catch (err) { + console.error('Failed to get UTM parameters:', err); + return {}; + } + }, + + // Page tracking methods + capturePageVisit: () => { + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + return false; + } + + try { + window.ThriveStack.capturePageVisit(); + return true; + } catch (err) { + console.error('Failed to capture page visit:', err); + return false; + } + }, + + // Debug methods + enableDebugMode: () => { + if (!window.ThriveStack) { + return false; + } + + try { + window.ThriveStack.enableDebugMode(); + return true; + } catch (err) { + console.error('Failed to enable debug mode:', err); + return false; + } + }, + + // Get ThriveStack instance (escape hatch) + getThriveStackInstance: () => { + return window.ThriveStack; + }, + + // Validate website domain manually + validateWebsite: async () => { + return await validateWebsite(); + } + } + }; + + return plugin; +} + +export default thriveStackPlugin \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/src/node.js b/packages/analytics-plugin-thrivestack/src/node.js new file mode 100644 index 00000000..8ed59ae0 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/src/node.js @@ -0,0 +1,396 @@ +/** + * ThriveStack analytics Node.js integration + * @param {object} pluginConfig - Plugin settings + * @param {string} pluginConfig.apiKey - ThriveStack API key + * @param {object} [pluginConfig.apiEndpoint] - Custom API endpoint + * @param {object} [pluginConfig.options] - ThriveStack options + * @returns {object} Analytics plugin + */ +function thriveStackPlugin(pluginConfig = {}) { + if (!pluginConfig.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + + let instance = null; + let initCompleted = false; + const API_URL = pluginConfig.apiEndpoint || 'https://api.app.thrivestack.ai/api'; + let API_KEY = pluginConfig.apiKey; + const options = { + respectDoNotTrack: pluginConfig.respectDoNotTrack !== false, + source: pluginConfig.source || '', + batchSize: pluginConfig.batchSize || 10, + batchInterval: pluginConfig.batchInterval || 2000, + ...pluginConfig.options + }; + + let userId = ''; + let anonymousId = 'server_' + Math.random().toString(36).substring(2, 15); + let groupId = ''; + const eventQueue = []; + let queueTimer = null; + + const initComplete = (i) => { + instance = i; + initCompleted = true; + console.log('ThriveStack initialization completed (server-side)'); + }; + + // Generate server-side IDs + const generateDeviceId = () => { + return 'server_device_' + Math.random().toString(36).substring(2, 15); + }; + + const generateSessionId = () => { + return 'server_session_' + Math.random().toString(36).substring(2, 15); + }; + + // Queue events for batch processing + const queueEvent = (events) => { + eventQueue.push(...events); + + if (eventQueue.length >= options.batchSize) { + processQueue(); + } else if (!queueTimer) { + queueTimer = setTimeout(() => processQueue(), options.batchInterval); + } + }; + + // Process queued events + const processQueue = async () => { + if (eventQueue.length === 0) return; + + const eventsToProcess = [...eventQueue]; + eventQueue.length = 0; + clearTimeout(queueTimer); + queueTimer = null; + + try { + await sendEvents(eventsToProcess); + } catch (error) { + console.error('Failed to process event queue:', error); + // Put events back in the queue + eventQueue.unshift(...eventsToProcess); + } + }; + + // Send events to ThriveStack API + const sendEvents = async (events) => { + const fetch = require('node-fetch'); + + try { + const response = await fetch(`${API_URL}/events`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` + }, + body: JSON.stringify(events) + }); + + if (!response.ok) { + throw new Error(`HTTP error ${response.status}: ${await response.text()}`); + } + + return await response.json(); + } catch (error) { + console.error('Error sending events to ThriveStack:', error); + throw error; + } + }; + + // Create plugin object + const plugin = { + name: 'thrivestack', + config: { + ...pluginConfig, + options + }, + + initialize: ({ config, instance: analyticsInstance }) => { + if (!config.apiKey) { + throw new Error("API Key is required for analytics plugin initialization"); + } + + API_KEY = config.apiKey; + + // Initialize is simpler in Node.js - no script to load + initComplete(analyticsInstance); + initCompleted = true; + + console.log('ThriveStack plugin initialized (server-side)'); + }, + + // Core methods + + identify: ({ payload }) => { + const { userId: id, traits = {}, options: opts = {} } = payload; + userId = id || ''; + + try { + const deviceId = generateDeviceId(); + const sessionId = generateSessionId(); + const timestamp = opts.timestamp || new Date().toISOString(); + const source = opts.source || options.source; + + const identifyPayload = [{ + user_id: userId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + device_id: deviceId, + session_id: sessionId, + source: source + } + }]; + + const fetch = require('node-fetch'); + return fetch(`${API_URL}/identify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` + }, + body: JSON.stringify(identifyPayload) + }) + .then(res => res.json()) + .then(result => { + console.log('Identify sent successfully (server-side):', result); + return result; + }) + .catch(err => { + console.error('Failed to send identify event (server-side):', err); + throw err; + }); + } catch (err) { + console.error('Failed to send identify event (server-side):', err); + throw err; + } + }, + + track: ({ payload }) => { + const { event, properties = {}, options: opts = {} } = payload; + + try { + const deviceId = generateDeviceId(); + const sessionId = generateSessionId(); + const timestamp = opts.timestamp || new Date().toISOString(); + const eventUserId = opts.userId || opts.user_id || userId; + const eventGroupId = opts.groupId || opts.group_id || groupId; + const source = opts.source || options.source; + + const eventPayload = [{ + event_name: event, + properties: properties, + user_id: eventUserId, + context: { + group_id: eventGroupId, + device_id: deviceId, + session_id: sessionId, + source: source + }, + timestamp: timestamp + }]; + + queueEvent(eventPayload); + return Promise.resolve({ queued: true }); + } catch (err) { + console.error('Failed to queue track event (server-side):', err); + return Promise.reject(err); + } + }, + + page: ({ payload }) => { + const { properties = {}, options: opts = {} } = payload; + + try { + const deviceId = generateDeviceId(); + const sessionId = generateSessionId(); + const timestamp = opts.timestamp || new Date().toISOString(); + const pageUserId = opts.userId || opts.user_id || userId; + const pageGroupId = opts.groupId || opts.group_id || groupId; + const source = opts.source || options.source; + + const pagePayload = [{ + event_name: "page_view", + properties: { + title: properties.title || '', + url: properties.url || '', + path: properties.path || '', + referrer: properties.referrer || '', + ...properties + }, + user_id: pageUserId, + context: { + group_id: pageGroupId, + device_id: deviceId, + session_id: sessionId, + source: source + }, + timestamp: timestamp + }]; + + queueEvent(pagePayload); + return Promise.resolve({ queued: true }); + } catch (err) { + console.error('Failed to queue page event (server-side):', err); + return Promise.reject(err); + } + }, + + reset: (payload, next) => { + userId = ''; + groupId = ''; + + console.log('ThriveStack data reset (server-side)'); + if (next) next(payload); + return Promise.resolve({ success: true }); + }, + + ready: () => { + return initCompleted; + }, + + // Storage methods (simplified for Node.js environment) + storage: { + getItem: (key) => { + // Could implement Redis/database storage for server-side + return null; + }, + + setItem: (key, value) => { + // Could implement Redis/database storage for server-side + return false; + }, + + removeItem: (key) => { + // Could implement Redis/database storage for server-side + return false; + } + }, + + // Context methods + setAnonymousId: (id) => { + anonymousId = id; + return true; + }, + + // User methods + user: () => { + return { + id: userId || null, + anonymousId: anonymousId, + isAuthenticated: !!userId + }; + }, + + // Plugin-specific methods + methods: { + // Group identification + groupIdentify: (groupId, traits = {}, options = {}, callback) => { + try { + const deviceId = generateDeviceId(); + const sessionId = generateSessionId(); + const timestamp = options.timestamp || new Date().toISOString(); + const groupUserId = options.userId || options.user_id || userId; + const source = options.source || ''; + + const groupPayload = [{ + user_id: groupUserId, + group_id: groupId, + traits: traits, + timestamp: timestamp, + context: { + group_id: groupId, + group_type: traits.group_type, + device_id: deviceId, + session_id: sessionId, + source: source + } + }]; + + const fetch = require('node-fetch'); + return fetch(`${API_URL}/group`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` + }, + body: JSON.stringify(groupPayload) + }) + .then(res => res.json()) + .then(result => { + console.log('Group identify sent successfully (server-side):', result); + if (callback && typeof callback === 'function') { + callback(null, result); + } + return result; + }) + .catch(err => { + console.error('Failed to send group identify (server-side):', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + } catch (err) { + console.error('Failed to send group identify (server-side):', err); + if (callback && typeof callback === 'function') { + callback(err); + } + return Promise.reject(err); + } + }, + + setApiConfig: (config = {}) => { + if (config.apiKey) { + API_KEY = config.apiKey; + } + + if (config.source) { + options.source = config.source; + } + + return true; + }, + + setSource: (source) => { + if (typeof source === 'string') { + options.source = source; + return true; + } + return false; + }, + + getUserId: () => { + return userId; + }, + + getGroupId: () => { + return groupId; + }, + + getSource: () => { + return options.source; + }, + + // Additional server-side specific methods + getAnonymousId: () => { + return anonymousId; + }, + + // Flush queued events immediately + flush: async () => { + if (queueTimer) { + clearTimeout(queueTimer); + queueTimer = null; + } + return processQueue(); + } + } + }; + + return plugin; +} + +module.exports = thriveStackPlugin; \ No newline at end of file diff --git a/packages/analytics-plugin-thrivestack/tsconfig.json b/packages/analytics-plugin-thrivestack/tsconfig.json new file mode 100644 index 00000000..ab55b965 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "declaration": true, + "sourceMap": true, + "outDir": "lib", + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "baseUrl": "src", + "paths": { + "./api": ["./api.js"], + ".": ["./index.js"] + }, + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.js", + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "lib", + "**/*.test.ts" + ] + } \ No newline at end of file