From 409efd107b75e41461a4481175f667549bd04d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Mon, 5 May 2025 19:23:58 +0530 Subject: [PATCH 1/8] TS plugin initial Commit --- .../analytics-plugin-thrivestack/README.md | 178 +++++ .../analytics-plugin-thrivestack/index.d.ts | 167 ++++ .../index.test.ts | 135 ++++ .../lib/index.browser.es.js | 646 ++++++++++++++++ .../lib/index.browser.js | 648 ++++++++++++++++ .../lib/index.es.js | 646 ++++++++++++++++ .../analytics-plugin-thrivestack/lib/index.js | 648 ++++++++++++++++ .../analytics-plugin-thrivestack/package.json | 35 + .../analytics-plugin-thrivestack/src/api.js | 121 +++ .../src/browser.js | 7 + .../analytics-plugin-thrivestack/src/index.js | 710 ++++++++++++++++++ .../analytics-plugin-thrivestack/src/node.js | 396 ++++++++++ .../tsconfig.json | 41 + 13 files changed, 4378 insertions(+) create mode 100644 packages/analytics-plugin-thrivestack/README.md create mode 100644 packages/analytics-plugin-thrivestack/index.d.ts create mode 100644 packages/analytics-plugin-thrivestack/index.test.ts create mode 100644 packages/analytics-plugin-thrivestack/lib/index.browser.es.js create mode 100644 packages/analytics-plugin-thrivestack/lib/index.browser.js create mode 100644 packages/analytics-plugin-thrivestack/lib/index.es.js create mode 100644 packages/analytics-plugin-thrivestack/lib/index.js create mode 100644 packages/analytics-plugin-thrivestack/package.json create mode 100644 packages/analytics-plugin-thrivestack/src/api.js create mode 100644 packages/analytics-plugin-thrivestack/src/browser.js create mode 100644 packages/analytics-plugin-thrivestack/src/index.js create mode 100644 packages/analytics-plugin-thrivestack/src/node.js create mode 100644 packages/analytics-plugin-thrivestack/tsconfig.json diff --git a/packages/analytics-plugin-thrivestack/README.md b/packages/analytics-plugin-thrivestack/README.md new file mode 100644 index 00000000..f98aa01d --- /dev/null +++ b/packages/analytics-plugin-thrivestack/README.md @@ -0,0 +1,178 @@ +# 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: 'awesome-app', + plugins: [ + thriveStackPlugin({ + apiKey: 'your-thrivestack-api-key', + options: { + // Additional configuration + 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() +``` + +## Configuration Options + +The ThriveStack plugin accepts the following configuration options: + +| Option | Description | Required | Default Value | +|:------|:------------|:---------|:--------------| +| `apiKey` | Your ThriveStack API key | Yes | - | +| `respectDoNotTrack` | Whether to respect DNT browser setting | No | `true` | +| `trackClicks` | Automatically track click events | No | `false` | +| `trackForms` | Automatically track form submissions | No | `false` | +| `enableConsent` | Enable consent management | No | `false` | +| `defaultConsent` | Default consent value | No | `false` | +| `source` | Source identifier | No | `''` | +| `batchSize` | Number of events to batch together | No | `10` | +| `batchInterval` | Interval in ms for processing event queue | No | `2000` | +| `options` | Additional options object | No | `{}` | + +## 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..52e6648f --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -0,0 +1,646 @@ +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'); + }; + + // Helper function to check if current domain matches validated domain + const isValidDomain = () => { + const validatedUrl = localStorage.getItem('website_url'); + if (!validatedUrl) { + console.warn('No validated website URL found in localStorage.'); + return false; + } + const currentHost = window.location.hostname; + const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; + console.log("9999", currentHost, allowedDomain); + return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + }; + + // Fetch onboarding step details and validate website domain + const validateWebsite = async () => { + try { + const productId = 'mZvcguoUd'; + const environmentId = 'GBASnf2UQ'; + const stepId = 'name_your_website'; + const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId + isCompleted + value + } + } + } + } + }`, + variables: { + input: { + productId: productId, + environmentId: environmentId, + stepId: stepId + } + } + }) + }); + const result = await response.json(); + console.log("API Response:", result); + if (result.data && result.data.getCAOnboardingStepDetails) { + const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; + + // Find the name_your_website step + for (const module of moduleDetails) { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + const stepValue = JSON.parse(step.value); + if (stepValue.website_url) { + localStorage.setItem('website_url', stepValue.website_url); + const currentHost = window.location.hostname; + const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + return false; + } + } + } + } + } + } + return false; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + + // Load ThriveStack script function + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + // Check if script is already loaded + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + + // Create script element + 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'); + } + + // Set up onload handler + 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')); + } + }; + + // Set up error handler + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + + // Append script to head + document.head.appendChild(script); + }); + }; + + // Process the event queue + 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 = []; + }; + + // Create plugin object + 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'); + + // Initialize ThriveStack + if (window.ThriveStack) { + // If ThriveStack's init method takes parameters, pass them + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + + // Validate website domain + const isValid = await validateWebsite(); + if (!isValid) { + console.warn('Website domain validation failed. Analytics calls will be blocked.'); + } + + // Set initialization complete + initComplete(analyticsInstance); + initCompleted = true; + + // Process queued events + 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; + if (!isValidDomain()) { + console.warn('Website domain not validated. Identify call blocked.'); + return; + } + try { + // Get deviceId without fallback + const deviceId = window.ThriveStack.getDeviceId(); + + // Validation: deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + 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; + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Track call blocked.'); + return; + } + try { + // Get the IDs without fallback to empty string + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: userId or deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + context: { + group_id: groupId, + device_id: deviceId || '', + // Use empty string only when sending the payload + 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; + } + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Page call blocked.'); + return; + } + try { + // ThriveStack has its own page tracking, so we can tap into that + // or explicitly trigger a page event + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + // Additional methods based on Analytics package spec + + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + + // Clear cookies if necessary + 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`; + 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 => { + // Called when analytics instance is fully initialized + return initCompleted; + }, + // Storage methods + storage: { + getItem: key => { + try { + return localStorage.getItem(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + localStorage.setItem(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + localStorage.removeItem(`thrivestack_${key}`); + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + // Context methods + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + try { + // ThriveStack uses device ID as anonymous ID + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + // ThriveStack doesn't allow setting device ID externally + console.warn('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 methods + user: () => { + if (!window.ThriveStack) return null; + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + // Plugin-specific methods + methods: { + // Group identification() + 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); + } + + // Check domain validation before processing + if (!isValidDomain()) { + const error = new Error('Website domain not validated. Group call blocked.'); + console.warn(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + // Get userId without fallback + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: both userId and groupId must be present + if (!userId) { + const error = new Error('Group identify requires userId to be present'); + console.error('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'); + console.error('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 => { + console.error('Failed to send group event:', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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: () => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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..ad2440fa --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -0,0 +1,648 @@ +'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'); + }; + + // Helper function to check if current domain matches validated domain + const isValidDomain = () => { + const validatedUrl = localStorage.getItem('website_url'); + if (!validatedUrl) { + console.warn('No validated website URL found in localStorage.'); + return false; + } + const currentHost = window.location.hostname; + const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; + console.log("9999", currentHost, allowedDomain); + return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + }; + + // Fetch onboarding step details and validate website domain + const validateWebsite = async () => { + try { + const productId = 'mZvcguoUd'; + const environmentId = 'GBASnf2UQ'; + const stepId = 'name_your_website'; + const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId + isCompleted + value + } + } + } + } + }`, + variables: { + input: { + productId: productId, + environmentId: environmentId, + stepId: stepId + } + } + }) + }); + const result = await response.json(); + console.log("API Response:", result); + if (result.data && result.data.getCAOnboardingStepDetails) { + const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; + + // Find the name_your_website step + for (const module of moduleDetails) { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + const stepValue = JSON.parse(step.value); + if (stepValue.website_url) { + localStorage.setItem('website_url', stepValue.website_url); + const currentHost = window.location.hostname; + const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + return false; + } + } + } + } + } + } + return false; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + + // Load ThriveStack script function + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + // Check if script is already loaded + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + + // Create script element + 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'); + } + + // Set up onload handler + 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')); + } + }; + + // Set up error handler + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + + // Append script to head + document.head.appendChild(script); + }); + }; + + // Process the event queue + 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 = []; + }; + + // Create plugin object + 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'); + + // Initialize ThriveStack + if (window.ThriveStack) { + // If ThriveStack's init method takes parameters, pass them + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + + // Validate website domain + const isValid = await validateWebsite(); + if (!isValid) { + console.warn('Website domain validation failed. Analytics calls will be blocked.'); + } + + // Set initialization complete + initComplete(analyticsInstance); + initCompleted = true; + + // Process queued events + 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; + if (!isValidDomain()) { + console.warn('Website domain not validated. Identify call blocked.'); + return; + } + try { + // Get deviceId without fallback + const deviceId = window.ThriveStack.getDeviceId(); + + // Validation: deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + 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; + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Track call blocked.'); + return; + } + try { + // Get the IDs without fallback to empty string + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: userId or deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + context: { + group_id: groupId, + device_id: deviceId || '', + // Use empty string only when sending the payload + 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; + } + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Page call blocked.'); + return; + } + try { + // ThriveStack has its own page tracking, so we can tap into that + // or explicitly trigger a page event + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + // Additional methods based on Analytics package spec + + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + + // Clear cookies if necessary + 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`; + 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 => { + // Called when analytics instance is fully initialized + return initCompleted; + }, + // Storage methods + storage: { + getItem: key => { + try { + return localStorage.getItem(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + localStorage.setItem(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + localStorage.removeItem(`thrivestack_${key}`); + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + // Context methods + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + try { + // ThriveStack uses device ID as anonymous ID + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + // ThriveStack doesn't allow setting device ID externally + console.warn('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 methods + user: () => { + if (!window.ThriveStack) return null; + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + // Plugin-specific methods + methods: { + // Group identification() + 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); + } + + // Check domain validation before processing + if (!isValidDomain()) { + const error = new Error('Website domain not validated. Group call blocked.'); + console.warn(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + // Get userId without fallback + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: both userId and groupId must be present + if (!userId) { + const error = new Error('Group identify requires userId to be present'); + console.error('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'); + console.error('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 => { + console.error('Failed to send group event:', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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: () => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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..52e6648f --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -0,0 +1,646 @@ +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'); + }; + + // Helper function to check if current domain matches validated domain + const isValidDomain = () => { + const validatedUrl = localStorage.getItem('website_url'); + if (!validatedUrl) { + console.warn('No validated website URL found in localStorage.'); + return false; + } + const currentHost = window.location.hostname; + const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; + console.log("9999", currentHost, allowedDomain); + return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + }; + + // Fetch onboarding step details and validate website domain + const validateWebsite = async () => { + try { + const productId = 'mZvcguoUd'; + const environmentId = 'GBASnf2UQ'; + const stepId = 'name_your_website'; + const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId + isCompleted + value + } + } + } + } + }`, + variables: { + input: { + productId: productId, + environmentId: environmentId, + stepId: stepId + } + } + }) + }); + const result = await response.json(); + console.log("API Response:", result); + if (result.data && result.data.getCAOnboardingStepDetails) { + const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; + + // Find the name_your_website step + for (const module of moduleDetails) { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + const stepValue = JSON.parse(step.value); + if (stepValue.website_url) { + localStorage.setItem('website_url', stepValue.website_url); + const currentHost = window.location.hostname; + const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + return false; + } + } + } + } + } + } + return false; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + + // Load ThriveStack script function + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + // Check if script is already loaded + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + + // Create script element + 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'); + } + + // Set up onload handler + 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')); + } + }; + + // Set up error handler + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + + // Append script to head + document.head.appendChild(script); + }); + }; + + // Process the event queue + 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 = []; + }; + + // Create plugin object + 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'); + + // Initialize ThriveStack + if (window.ThriveStack) { + // If ThriveStack's init method takes parameters, pass them + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + + // Validate website domain + const isValid = await validateWebsite(); + if (!isValid) { + console.warn('Website domain validation failed. Analytics calls will be blocked.'); + } + + // Set initialization complete + initComplete(analyticsInstance); + initCompleted = true; + + // Process queued events + 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; + if (!isValidDomain()) { + console.warn('Website domain not validated. Identify call blocked.'); + return; + } + try { + // Get deviceId without fallback + const deviceId = window.ThriveStack.getDeviceId(); + + // Validation: deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + 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; + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Track call blocked.'); + return; + } + try { + // Get the IDs without fallback to empty string + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: userId or deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + context: { + group_id: groupId, + device_id: deviceId || '', + // Use empty string only when sending the payload + 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; + } + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Page call blocked.'); + return; + } + try { + // ThriveStack has its own page tracking, so we can tap into that + // or explicitly trigger a page event + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + // Additional methods based on Analytics package spec + + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + + // Clear cookies if necessary + 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`; + 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 => { + // Called when analytics instance is fully initialized + return initCompleted; + }, + // Storage methods + storage: { + getItem: key => { + try { + return localStorage.getItem(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + localStorage.setItem(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + localStorage.removeItem(`thrivestack_${key}`); + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + // Context methods + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + try { + // ThriveStack uses device ID as anonymous ID + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + // ThriveStack doesn't allow setting device ID externally + console.warn('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 methods + user: () => { + if (!window.ThriveStack) return null; + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + // Plugin-specific methods + methods: { + // Group identification() + 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); + } + + // Check domain validation before processing + if (!isValidDomain()) { + const error = new Error('Website domain not validated. Group call blocked.'); + console.warn(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + // Get userId without fallback + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: both userId and groupId must be present + if (!userId) { + const error = new Error('Group identify requires userId to be present'); + console.error('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'); + console.error('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 => { + console.error('Failed to send group event:', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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: () => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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..ad2440fa --- /dev/null +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -0,0 +1,648 @@ +'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'); + }; + + // Helper function to check if current domain matches validated domain + const isValidDomain = () => { + const validatedUrl = localStorage.getItem('website_url'); + if (!validatedUrl) { + console.warn('No validated website URL found in localStorage.'); + return false; + } + const currentHost = window.location.hostname; + const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; + console.log("9999", currentHost, allowedDomain); + return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + }; + + // Fetch onboarding step details and validate website domain + const validateWebsite = async () => { + try { + const productId = 'mZvcguoUd'; + const environmentId = 'GBASnf2UQ'; + const stepId = 'name_your_website'; + const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId + isCompleted + value + } + } + } + } + }`, + variables: { + input: { + productId: productId, + environmentId: environmentId, + stepId: stepId + } + } + }) + }); + const result = await response.json(); + console.log("API Response:", result); + if (result.data && result.data.getCAOnboardingStepDetails) { + const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; + + // Find the name_your_website step + for (const module of moduleDetails) { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + const stepValue = JSON.parse(step.value); + if (stepValue.website_url) { + localStorage.setItem('website_url', stepValue.website_url); + const currentHost = window.location.hostname; + const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + return false; + } + } + } + } + } + } + return false; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + + // Load ThriveStack script function + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + // Check if script is already loaded + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + + // Create script element + 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'); + } + + // Set up onload handler + 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')); + } + }; + + // Set up error handler + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + + // Append script to head + document.head.appendChild(script); + }); + }; + + // Process the event queue + 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 = []; + }; + + // Create plugin object + 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'); + + // Initialize ThriveStack + if (window.ThriveStack) { + // If ThriveStack's init method takes parameters, pass them + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + console.log('ThriveStack plugin initialized'); + } + + // Validate website domain + const isValid = await validateWebsite(); + if (!isValid) { + console.warn('Website domain validation failed. Analytics calls will be blocked.'); + } + + // Set initialization complete + initComplete(analyticsInstance); + initCompleted = true; + + // Process queued events + 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; + if (!isValidDomain()) { + console.warn('Website domain not validated. Identify call blocked.'); + return; + } + try { + // Get deviceId without fallback + const deviceId = window.ThriveStack.getDeviceId(); + + // Validation: deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + 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; + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Track call blocked.'); + return; + } + try { + // Get the IDs without fallback to empty string + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: userId or deviceId must be present + if (!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 || '', + // Use empty string only when sending the payload + context: { + group_id: groupId, + device_id: deviceId || '', + // Use empty string only when sending the payload + 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; + } + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Page call blocked.'); + return; + } + try { + // ThriveStack has its own page tracking, so we can tap into that + // or explicitly trigger a page event + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + // Additional methods based on Analytics package spec + + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + try { + window.ThriveStack.setUserId(''); + window.ThriveStack.setGroupId(''); + + // Clear cookies if necessary + 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`; + 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 => { + // Called when analytics instance is fully initialized + return initCompleted; + }, + // Storage methods + storage: { + getItem: key => { + try { + return localStorage.getItem(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + setItem: (key, value) => { + try { + localStorage.setItem(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + removeItem: key => { + try { + localStorage.removeItem(`thrivestack_${key}`); + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + // Context methods + setAnonymousId: anonymousId => { + if (!window.ThriveStack) return false; + try { + // ThriveStack uses device ID as anonymous ID + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + // ThriveStack doesn't allow setting device ID externally + console.warn('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 methods + user: () => { + if (!window.ThriveStack) return null; + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + // Plugin-specific methods + methods: { + // Group identification() + 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); + } + + // Check domain validation before processing + if (!isValidDomain()) { + const error = new Error('Website domain not validated. Group call blocked.'); + console.warn(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + // Get userId without fallback + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: both userId and groupId must be present + if (!userId) { + const error = new Error('Group identify requires userId to be present'); + console.error('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'); + console.error('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 => { + console.error('Failed to send group event:', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + setApiConfig: (config = {}) => { + if (config.apiKey) { + API_KEY = config.apiKey; + } + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + return true; + }, + setConsent: (category, enabled) => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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: () => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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..cf7504cb --- /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" + } + } \ No newline at end of file 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..6deadb97 --- /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..1e846287 --- /dev/null +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -0,0 +1,710 @@ +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'); + }; + + // Helper function to check if current domain matches validated domain + const isValidDomain = () => { + const validatedUrl = localStorage.getItem('website_url'); + if (!validatedUrl) { + console.warn('No validated website URL found in localStorage.'); + return false; + } + + const currentHost = window.location.hostname; + const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; + + console.log("9999", currentHost, allowedDomain) + + return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + }; + + // Fetch onboarding step details and validate website domain + const validateWebsite = async () => { + try { + const productId = 'mZvcguoUd'; + const environmentId = 'GBASnf2UQ'; + const stepId = 'name_your_website'; + + const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId + isCompleted + value + } + } + } + } + }`, + variables: { + input: { + productId: productId, + environmentId: environmentId, + stepId: stepId + } + } + }) + }); + + const result = await response.json(); + console.log("API Response:", result); + + if (result.data && result.data.getCAOnboardingStepDetails) { + const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; + + // Find the name_your_website step + for (const module of moduleDetails) { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + const stepValue = JSON.parse(step.value); + if (stepValue.website_url) { + localStorage.setItem('website_url', stepValue.website_url); + + const currentHost = window.location.hostname; + const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; + + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + return false; + } + } + } + } + } + } + + return false; + } catch (error) { + console.error('Failed to validate website:', error); + return false; + } + }; + + // Load ThriveStack script function + const loadThriveStackScript = () => { + return new Promise((resolve, reject) => { + // Check if script is already loaded + if (window.ThriveStack) { + resolve(window.ThriveStack); + return; + } + + // Create script element + 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'); + } + + // Set up onload handler + 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')); + } + }; + + // Set up error handler + script.onerror = () => { + reject(new Error('Failed to load ThriveStack script')); + }; + + // Append script to head + document.head.appendChild(script); + }); + }; + + // Process the event queue + 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 = []; + }; + + // Create plugin object + 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'); + + // Initialize ThriveStack + if (window.ThriveStack) { + // If ThriveStack's init method takes parameters, pass them + if (config.userId) { + await window.ThriveStack.init(config.userId, options.source || ''); + } else { + await window.ThriveStack.init(); + } + + console.log('ThriveStack plugin initialized'); + } + + // Validate website domain + const isValid = await validateWebsite(); + + if (!isValid) { + console.warn('Website domain validation failed. Analytics calls will be blocked.'); + } + + // Set initialization complete + initComplete(analyticsInstance); + initCompleted = true; + + // Process queued events + 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; + + if (!isValidDomain()) { + console.warn('Website domain not validated. Identify call blocked.'); + return; + } + + try { + // Get deviceId without fallback + const deviceId = window.ThriveStack.getDeviceId(); + + // Validation: deviceId must be present + 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 || '', // Use empty string only when sending the payload + 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; + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Track call blocked.'); + return; + } + + try { + // Get the IDs without fallback to empty string + const deviceId = window.ThriveStack.getDeviceId(); + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: userId or deviceId must be present + 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 || '', // Use empty string only when sending the payload + context: { + group_id: groupId, + device_id: deviceId || '', // Use empty string only when sending the payload + 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; + + // Check domain validation before processing + if (!isValidDomain()) { + console.warn('Website domain not validated. Page call blocked.'); + return; + } + + try { + // ThriveStack has its own page tracking, so we can tap into that + // or explicitly trigger a page event + window.ThriveStack.capturePageVisit(); + } catch (err) { + console.error('Failed to send page event:', err); + } + }, + + // Additional methods based on Analytics package spec + + reset: (payload, next) => { + if (!window.ThriveStack) { + if (next) next(payload); + return; + } + + try { + window.ThriveStack.setUserId('') + window.ThriveStack.setGroupId('') + + // Clear cookies if necessary + 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`; + + 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) => { + // Called when analytics instance is fully initialized + return initCompleted; + }, + + // Storage methods + storage: { + getItem: (key) => { + try { + return localStorage.getItem(`thrivestack_${key}`); + } catch (err) { + console.error('Failed to get item from storage:', err); + return null; + } + }, + + setItem: (key, value) => { + try { + localStorage.setItem(`thrivestack_${key}`, value); + return true; + } catch (err) { + console.error('Failed to set item in storage:', err); + return false; + } + }, + + removeItem: (key) => { + try { + localStorage.removeItem(`thrivestack_${key}`); + return true; + } catch (err) { + console.error('Failed to remove item from storage:', err); + return false; + } + } + }, + + // Context methods + setAnonymousId: (anonymousId) => { + if (!window.ThriveStack) return false; + + try { + // ThriveStack uses device ID as anonymous ID + const currentDeviceId = window.ThriveStack.getDeviceId(); + if (currentDeviceId !== anonymousId) { + // ThriveStack doesn't allow setting device ID externally + console.warn('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 methods + user: () => { + if (!window.ThriveStack) return null; + + return { + id: window.ThriveStack.userId || null, + anonymousId: window.ThriveStack.getDeviceId() || null, + isAuthenticated: !!window.ThriveStack.userId + }; + }, + + + + // Plugin-specific methods + methods: { + // Group identification() + 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); + } + + // Check domain validation before processing + if (!isValidDomain()) { + const error = new Error('Website domain not validated. Group call blocked.'); + console.warn(error.message); + if (callback && typeof callback === 'function') { + callback(error); + } + return Promise.reject(error); + } + + // Get userId without fallback + const userId = options.userId || options.user_id || window.ThriveStack.userId; + + // Validation: both userId and groupId must be present + if (options.source === 'marketing' && !userId) { + const error = new Error('Group identify requires userId to be present'); + console.error('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'); + console.error('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 => { + console.error('Failed to send group event:', err); + if (callback && typeof callback === 'function') { + callback(err); + } + throw err; + }); + }, + + setApiConfig: (config = {}) => { + if (config.apiKey) { + API_KEY = config.apiKey; + } + + if (config.source && window.ThriveStack) { + window.ThriveStack.setSource(config.source); + } + + return true; + }, + + setConsent: (category, enabled) => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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: () => { + if (!window.ThriveStack || !isValidDomain()) { + console.warn('Website domain not validated. 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..16da9f7e --- /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.dev.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 From f824c0b5ab77cdbcef7602c9ba10128163f68442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Wed, 7 May 2025 15:25:23 +0530 Subject: [PATCH 2/8] Add: source verfication before all calls --- .../lib/index.browser.es.js | 63 ++++++++++++------ .../lib/index.browser.js | 63 ++++++++++++------ .../lib/index.es.js | 63 ++++++++++++------ .../analytics-plugin-thrivestack/lib/index.js | 63 ++++++++++++------ .../analytics-plugin-thrivestack/package.json | 32 ++++----- .../src/browser.js | 2 +- .../analytics-plugin-thrivestack/src/index.js | 66 ++++++++++++------- 7 files changed, 229 insertions(+), 123 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js index 52e6648f..65f5a4ee 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -22,24 +22,35 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack initialization completed'); }; - // Helper function to check if current domain matches validated domain + // Helper function to normalize URLs by removing protocol and anything after the first slash + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Helper function to check if current domain matches any validated domain const isValidDomain = () => { - const validatedUrl = localStorage.getItem('website_url'); - if (!validatedUrl) { - console.warn('No validated website URL found in localStorage.'); + const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); + if (!validatedUrls.length) { + console.warn('No validated website URLs found in localStorage.'); return false; } const currentHost = window.location.hostname; - const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; - console.log("9999", currentHost, allowedDomain); - return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + + // Check if current host matches any of the allowed domains + for (const allowedDomain of validatedUrls) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + return false; }; // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { - const productId = 'mZvcguoUd'; - const environmentId = 'GBASnf2UQ'; + const productId = 'X6EcdUZFY'; + const environmentId = '2lwIjczu1'; const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', @@ -85,23 +96,33 @@ function thriveStackPlugin(pluginConfig = {}) { for (const module of moduleDetails) { for (const step of module.stepDetails) { if (step.stepId === 'name_your_website' && step.value) { - const stepValue = JSON.parse(step.value); - if (stepValue.website_url) { - localStorage.setItem('website_url', stepValue.website_url); - const currentHost = window.location.hostname; - const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; - if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { - console.log('Website domain validated successfully'); - return true; + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); + const currentHost = window.location.hostname; + const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); + if (isValid) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); + return false; + } } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + console.warn('No website_urls array found in step value'); return false; } + } catch (parseError) { + console.error('Failed to parse step value JSON:', parseError); + return false; } } } } } + console.warn('No name_your_website step found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); @@ -247,7 +268,7 @@ function thriveStackPlugin(pluginConfig = {}) { const deviceId = window.ThriveStack.getDeviceId(); // Validation: deviceId must be present - if (!deviceId) { + 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') { @@ -306,7 +327,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: userId or deviceId must be present - if (!userId && !deviceId) { + 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') { @@ -470,7 +491,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: both userId and groupId must be present - if (!userId) { + if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); console.error('Group identify failed:', error); if (callback && typeof callback === 'function') { diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.js b/packages/analytics-plugin-thrivestack/lib/index.browser.js index ad2440fa..2a139a53 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -24,24 +24,35 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack initialization completed'); }; - // Helper function to check if current domain matches validated domain + // Helper function to normalize URLs by removing protocol and anything after the first slash + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Helper function to check if current domain matches any validated domain const isValidDomain = () => { - const validatedUrl = localStorage.getItem('website_url'); - if (!validatedUrl) { - console.warn('No validated website URL found in localStorage.'); + const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); + if (!validatedUrls.length) { + console.warn('No validated website URLs found in localStorage.'); return false; } const currentHost = window.location.hostname; - const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; - console.log("9999", currentHost, allowedDomain); - return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + + // Check if current host matches any of the allowed domains + for (const allowedDomain of validatedUrls) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + return false; }; // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { - const productId = 'mZvcguoUd'; - const environmentId = 'GBASnf2UQ'; + const productId = 'X6EcdUZFY'; + const environmentId = '2lwIjczu1'; const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', @@ -87,23 +98,33 @@ function thriveStackPlugin(pluginConfig = {}) { for (const module of moduleDetails) { for (const step of module.stepDetails) { if (step.stepId === 'name_your_website' && step.value) { - const stepValue = JSON.parse(step.value); - if (stepValue.website_url) { - localStorage.setItem('website_url', stepValue.website_url); - const currentHost = window.location.hostname; - const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; - if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { - console.log('Website domain validated successfully'); - return true; + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); + const currentHost = window.location.hostname; + const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); + if (isValid) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); + return false; + } } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + console.warn('No website_urls array found in step value'); return false; } + } catch (parseError) { + console.error('Failed to parse step value JSON:', parseError); + return false; } } } } } + console.warn('No name_your_website step found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); @@ -249,7 +270,7 @@ function thriveStackPlugin(pluginConfig = {}) { const deviceId = window.ThriveStack.getDeviceId(); // Validation: deviceId must be present - if (!deviceId) { + 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') { @@ -308,7 +329,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: userId or deviceId must be present - if (!userId && !deviceId) { + 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') { @@ -472,7 +493,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: both userId and groupId must be present - if (!userId) { + if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); console.error('Group identify failed:', error); if (callback && typeof callback === 'function') { diff --git a/packages/analytics-plugin-thrivestack/lib/index.es.js b/packages/analytics-plugin-thrivestack/lib/index.es.js index 52e6648f..65f5a4ee 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -22,24 +22,35 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack initialization completed'); }; - // Helper function to check if current domain matches validated domain + // Helper function to normalize URLs by removing protocol and anything after the first slash + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Helper function to check if current domain matches any validated domain const isValidDomain = () => { - const validatedUrl = localStorage.getItem('website_url'); - if (!validatedUrl) { - console.warn('No validated website URL found in localStorage.'); + const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); + if (!validatedUrls.length) { + console.warn('No validated website URLs found in localStorage.'); return false; } const currentHost = window.location.hostname; - const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; - console.log("9999", currentHost, allowedDomain); - return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + + // Check if current host matches any of the allowed domains + for (const allowedDomain of validatedUrls) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + return false; }; // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { - const productId = 'mZvcguoUd'; - const environmentId = 'GBASnf2UQ'; + const productId = 'X6EcdUZFY'; + const environmentId = '2lwIjczu1'; const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', @@ -85,23 +96,33 @@ function thriveStackPlugin(pluginConfig = {}) { for (const module of moduleDetails) { for (const step of module.stepDetails) { if (step.stepId === 'name_your_website' && step.value) { - const stepValue = JSON.parse(step.value); - if (stepValue.website_url) { - localStorage.setItem('website_url', stepValue.website_url); - const currentHost = window.location.hostname; - const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; - if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { - console.log('Website domain validated successfully'); - return true; + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); + const currentHost = window.location.hostname; + const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); + if (isValid) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); + return false; + } } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + console.warn('No website_urls array found in step value'); return false; } + } catch (parseError) { + console.error('Failed to parse step value JSON:', parseError); + return false; } } } } } + console.warn('No name_your_website step found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); @@ -247,7 +268,7 @@ function thriveStackPlugin(pluginConfig = {}) { const deviceId = window.ThriveStack.getDeviceId(); // Validation: deviceId must be present - if (!deviceId) { + 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') { @@ -306,7 +327,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: userId or deviceId must be present - if (!userId && !deviceId) { + 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') { @@ -470,7 +491,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: both userId and groupId must be present - if (!userId) { + if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); console.error('Group identify failed:', error); if (callback && typeof callback === 'function') { diff --git a/packages/analytics-plugin-thrivestack/lib/index.js b/packages/analytics-plugin-thrivestack/lib/index.js index ad2440fa..2a139a53 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.js +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -24,24 +24,35 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack initialization completed'); }; - // Helper function to check if current domain matches validated domain + // Helper function to normalize URLs by removing protocol and anything after the first slash + const normalizeUrl = url => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Helper function to check if current domain matches any validated domain const isValidDomain = () => { - const validatedUrl = localStorage.getItem('website_url'); - if (!validatedUrl) { - console.warn('No validated website URL found in localStorage.'); + const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); + if (!validatedUrls.length) { + console.warn('No validated website URLs found in localStorage.'); return false; } const currentHost = window.location.hostname; - const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; - console.log("9999", currentHost, allowedDomain); - return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + + // Check if current host matches any of the allowed domains + for (const allowedDomain of validatedUrls) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + return false; }; // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { - const productId = 'mZvcguoUd'; - const environmentId = 'GBASnf2UQ'; + const productId = 'X6EcdUZFY'; + const environmentId = '2lwIjczu1'; const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', @@ -87,23 +98,33 @@ function thriveStackPlugin(pluginConfig = {}) { for (const module of moduleDetails) { for (const step of module.stepDetails) { if (step.stepId === 'name_your_website' && step.value) { - const stepValue = JSON.parse(step.value); - if (stepValue.website_url) { - localStorage.setItem('website_url', stepValue.website_url); - const currentHost = window.location.hostname; - const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; - if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { - console.log('Website domain validated successfully'); - return true; + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); + const currentHost = window.location.hostname; + const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); + if (isValid) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); + return false; + } } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + console.warn('No website_urls array found in step value'); return false; } + } catch (parseError) { + console.error('Failed to parse step value JSON:', parseError); + return false; } } } } } + console.warn('No name_your_website step found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); @@ -249,7 +270,7 @@ function thriveStackPlugin(pluginConfig = {}) { const deviceId = window.ThriveStack.getDeviceId(); // Validation: deviceId must be present - if (!deviceId) { + 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') { @@ -308,7 +329,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: userId or deviceId must be present - if (!userId && !deviceId) { + 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') { @@ -472,7 +493,7 @@ function thriveStackPlugin(pluginConfig = {}) { const userId = options.userId || options.user_id || window.ThriveStack.userId; // Validation: both userId and groupId must be present - if (!userId) { + if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); console.error('Group identify failed:', error); if (callback && typeof callback === 'function') { diff --git a/packages/analytics-plugin-thrivestack/package.json b/packages/analytics-plugin-thrivestack/package.json index cf7504cb..9d84a9d9 100644 --- a/packages/analytics-plugin-thrivestack/package.json +++ b/packages/analytics-plugin-thrivestack/package.json @@ -3,33 +3,33 @@ "version": "0.1.0", "description": "ThriveStack integration for 'analytics' module", "keywords": [ - "analytics", - "analytics-plugin", - "thrivestack" + "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" + "./lib/index.js": "./lib/index.browser.js", + "./lib/index.es.js": "./lib/index.browser.es.js" }, "files": [ - "dist", - "lib", - "README.md" + "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" + "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" + "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" } - } \ No newline at end of file +} diff --git a/packages/analytics-plugin-thrivestack/src/browser.js b/packages/analytics-plugin-thrivestack/src/browser.js index 6deadb97..eb3f78b9 100644 --- a/packages/analytics-plugin-thrivestack/src/browser.js +++ b/packages/analytics-plugin-thrivestack/src/browser.js @@ -2,6 +2,6 @@ * Browser implementation for ThriveStack plugin */ -import thriveStackPlugin from '..' +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 index 1e846287..b8a99aed 100644 --- a/packages/analytics-plugin-thrivestack/src/index.js +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -27,27 +27,37 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack initialization completed'); }; - // Helper function to check if current domain matches validated domain + // Helper function to normalize URLs by removing protocol and anything after the first slash + const normalizeUrl = (url) => { + return url.replace(/^https?:\/\//, '').split('/')[0]; + }; + + // Helper function to check if current domain matches any validated domain const isValidDomain = () => { - const validatedUrl = localStorage.getItem('website_url'); - if (!validatedUrl) { - console.warn('No validated website URL found in localStorage.'); + const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); + if (!validatedUrls.length) { + console.warn('No validated website URLs found in localStorage.'); return false; } const currentHost = window.location.hostname; - const allowedDomain = validatedUrl.replace(/^https?:\/\//, '').split('/')[0]; - - console.log("9999", currentHost, allowedDomain) - return currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost); + // Check if current host matches any of the allowed domains + for (const allowedDomain of validatedUrls) { + if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { + return true; + } + } + + console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + return false; }; // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { - const productId = 'mZvcguoUd'; - const environmentId = 'GBASnf2UQ'; + const productId = 'X6EcdUZFY'; + const environmentId = '2lwIjczu1'; const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { @@ -96,26 +106,40 @@ function thriveStackPlugin(pluginConfig = {}) { for (const module of moduleDetails) { for (const step of module.stepDetails) { if (step.stepId === 'name_your_website' && step.value) { - const stepValue = JSON.parse(step.value); - if (stepValue.website_url) { - localStorage.setItem('website_url', stepValue.website_url); - - const currentHost = window.location.hostname; - const allowedDomain = stepValue.website_url.replace(/^https?:\/\//, '').split('/')[0]; + try { + const stepValue = JSON.parse(step.value); - if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { - console.log('Website domain validated successfully'); - return true; + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); + + const currentHost = window.location.hostname; + + const isValid = normalizedUrls.some(domain => + currentHost.includes(domain) || domain.includes(currentHost) + ); + + if (isValid) { + console.log('Website domain validated successfully'); + return true; + } else { + console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); + return false; + } } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${allowedDomain}`); + console.warn('No website_urls array found in step value'); return false; } + } catch (parseError) { + console.error('Failed to parse step value JSON:', parseError); + return false; } } } } } + console.warn('No name_your_website step found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); @@ -474,8 +498,6 @@ function thriveStackPlugin(pluginConfig = {}) { }; }, - - // Plugin-specific methods methods: { // Group identification() From c3f0253aea0666ddb518e7724da9d55c2bc03429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Wed, 7 May 2025 17:16:51 +0530 Subject: [PATCH 3/8] update: Domain verify & documentation file --- .../lib/index.browser.es.js | 215 +++++++-------- .../lib/index.browser.js | 215 +++++++-------- .../lib/index.es.js | 215 +++++++-------- .../analytics-plugin-thrivestack/lib/index.js | 215 +++++++-------- .../analytics-plugin-thrivestack/src/index.js | 254 ++++++++++-------- 5 files changed, 542 insertions(+), 572 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js index 65f5a4ee..0f40f3ef 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -21,37 +21,35 @@ function thriveStackPlugin(pluginConfig = {}) { initCompleted = true; console.log('ThriveStack initialization completed'); }; - - // Helper function to normalize URLs by removing protocol and anything after the first slash const normalizeUrl = url => { return url.replace(/^https?:\/\//, '').split('/')[0]; }; - - // Helper function to check if current domain matches any validated domain - const isValidDomain = () => { - const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); - if (!validatedUrls.length) { - console.warn('No validated website URLs found in localStorage.'); + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = JSON.parse(localStorage.getItem('product_urls') || '[]'); + } else if (source === 'marketing') { + urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); + } else { + urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + } + if (!urlsToCheck.length) { + console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); return false; } const currentHost = window.location.hostname; - - // Check if current host matches any of the allowed domains - for (const allowedDomain of validatedUrls) { + for (const allowedDomain of urlsToCheck) { if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; - - // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { const productId = 'X6EcdUZFY'; const environmentId = '2lwIjczu1'; - const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', headers: { @@ -60,29 +58,28 @@ function thriveStackPlugin(pluginConfig = {}) { }, body: JSON.stringify({ query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + value } } } - }`, + } + }`, variables: { input: { productId: productId, - environmentId: environmentId, - stepId: stepId + environmentId: environmentId } } }) @@ -91,55 +88,60 @@ function thriveStackPlugin(pluginConfig = {}) { console.log("API Response:", result); if (result.data && result.data.getCAOnboardingStepDetails) { const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - - // Find the name_your_website step + let marketingUrls = []; + let productUrls = []; for (const module of moduleDetails) { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); - const currentHost = window.location.hostname; - const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); - if (isValid) { - console.log('Website domain validated successfully'); - return true; - } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); - return false; + if (module.moduleId === 'marketing_attribution') { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing URLs JSON:', parseError); + } + } + } + } + if (module.moduleId === 'product_analytics') { + for (const step of module.stepDetails) { + for (const subStep of step.subStepDetails || []) { + if (subStep.subStepId === 'product_url' && subStep.value) { + try { + const subStepValue = JSON.parse(subStep.value); + if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { + productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product URLs JSON:', parseError); } - } else { - console.warn('No website_urls array found in step value'); - return false; } - } catch (parseError) { - console.error('Failed to parse step value JSON:', parseError); - return false; } } } } + localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); + localStorage.setItem('product_urls', JSON.stringify(productUrls)); + localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); + console.log('Stored product URLs:', productUrls); + console.log('Stored marketing URLs:', marketingUrls); + return true; } - console.warn('No name_your_website step found in API response'); + console.warn('No valid website URLs found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); return false; } }; - - // Load ThriveStack script function const loadThriveStackScript = () => { return new Promise((resolve, reject) => { - // Check if script is already loaded if (window.ThriveStack) { resolve(window.ThriveStack); return; } - - // Create script element const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; @@ -159,8 +161,6 @@ function thriveStackPlugin(pluginConfig = {}) { if (options.respectDoNotTrack !== undefined) { script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); } - - // Set up onload handler script.onload = () => { if (window.ThriveStack) { if (options.debug) { @@ -171,18 +171,12 @@ function thriveStackPlugin(pluginConfig = {}) { reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); } }; - - // Set up error handler script.onerror = () => { reject(new Error('Failed to load ThriveStack script')); }; - - // Append script to head document.head.appendChild(script); }); }; - - // Process the event queue const processQueue = () => { if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { return; @@ -198,8 +192,6 @@ function thriveStackPlugin(pluginConfig = {}) { }); window._tsq = []; }; - - // Create plugin object const plugin = { name: 'thrivestack', config: { @@ -214,10 +206,7 @@ function thriveStackPlugin(pluginConfig = {}) { try { await loadThriveStackScript(); console.log('ThriveStack script loaded successfully'); - - // Initialize ThriveStack if (window.ThriveStack) { - // If ThriveStack's init method takes parameters, pass them if (config.userId) { await window.ThriveStack.init(config.userId, options.source || ''); } else { @@ -225,25 +214,19 @@ function thriveStackPlugin(pluginConfig = {}) { } console.log('ThriveStack plugin initialized'); } - - // Validate website domain const isValid = await validateWebsite(); if (!isValid) { console.warn('Website domain validation failed. Analytics calls will be blocked.'); } - - // Set initialization complete initComplete(analyticsInstance); initCompleted = true; - - // Process queued events processQueue(); } catch (error) { console.error('Failed to initialize ThriveStack plugin:', error); throw error; } }, - // Core methods + // CORE-METHODS identify: ({ payload @@ -259,8 +242,9 @@ function thriveStackPlugin(pluginConfig = {}) { traits = {}, options = {} } = payload; - if (!isValidDomain()) { - console.warn('Website domain not validated. Identify call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -315,18 +299,14 @@ function thriveStackPlugin(pluginConfig = {}) { properties = {}, options = {} } = payload; - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Track call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get the IDs without fallback to empty string const deviceId = window.ThriveStack.getDeviceId(); const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: userId or deviceId must be present 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); @@ -342,11 +322,9 @@ function thriveStackPlugin(pluginConfig = {}) { event_name: event, properties: properties, user_id: userId || '', - // Use empty string only when sending the payload context: { group_id: groupId, device_id: deviceId || '', - // Use empty string only when sending the payload session_id: sessionId, source: source }, @@ -369,22 +347,21 @@ function thriveStackPlugin(pluginConfig = {}) { }]); return; } - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Page call blocked.'); + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { - // ThriveStack has its own page tracking, so we can tap into that - // or explicitly trigger a page event window.ThriveStack.capturePageVisit(); } catch (err) { console.error('Failed to send page event:', err); } }, - // Additional methods based on Analytics package spec - reset: (payload, next) => { if (!window.ThriveStack) { if (next) next(payload); @@ -393,8 +370,6 @@ function thriveStackPlugin(pluginConfig = {}) { try { window.ThriveStack.setUserId(''); window.ThriveStack.setGroupId(''); - - // Clear cookies if necessary 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`; @@ -408,10 +383,8 @@ function thriveStackPlugin(pluginConfig = {}) { } }, ready: payload => { - // Called when analytics instance is fully initialized return initCompleted; }, - // Storage methods storage: { getItem: key => { try { @@ -440,14 +413,16 @@ function thriveStackPlugin(pluginConfig = {}) { } } }, - // Context methods setAnonymousId: anonymousId => { if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } try { - // ThriveStack uses device ID as anonymous ID const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - // ThriveStack doesn't allow setting device ID externally console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; @@ -456,18 +431,20 @@ function thriveStackPlugin(pluginConfig = {}) { return false; } }, - // User methods user: () => { if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`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 }; }, - // Plugin-specific methods methods: { - // Group identification() group: (groupId, traits = {}, options = {}, callback) => { if (!window.ThriveStack) { const error = new Error('ThriveStack not initialized'); @@ -476,10 +453,11 @@ function thriveStackPlugin(pluginConfig = {}) { } return Promise.reject(error); } + const source = options.source || window.ThriveStack.getSource() || ''; // Check domain validation before processing - if (!isValidDomain()) { - const error = new Error('Website domain not validated. Group call blocked.'); + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); console.warn(error.message); if (callback && typeof callback === 'function') { callback(error); @@ -535,6 +513,11 @@ function thriveStackPlugin(pluginConfig = {}) { }); }, setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } if (config.apiKey) { API_KEY = config.apiKey; } @@ -544,8 +527,9 @@ function thriveStackPlugin(pluginConfig = {}) { return true; }, setConsent: (category, enabled) => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. setConsent call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -626,8 +610,9 @@ function thriveStackPlugin(pluginConfig = {}) { }, // Page tracking methods capturePageVisit: () => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. capturePageVisit call blocked.'); + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.js b/packages/analytics-plugin-thrivestack/lib/index.browser.js index 2a139a53..28097e20 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -23,37 +23,35 @@ function thriveStackPlugin(pluginConfig = {}) { initCompleted = true; console.log('ThriveStack initialization completed'); }; - - // Helper function to normalize URLs by removing protocol and anything after the first slash const normalizeUrl = url => { return url.replace(/^https?:\/\//, '').split('/')[0]; }; - - // Helper function to check if current domain matches any validated domain - const isValidDomain = () => { - const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); - if (!validatedUrls.length) { - console.warn('No validated website URLs found in localStorage.'); + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = JSON.parse(localStorage.getItem('product_urls') || '[]'); + } else if (source === 'marketing') { + urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); + } else { + urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + } + if (!urlsToCheck.length) { + console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); return false; } const currentHost = window.location.hostname; - - // Check if current host matches any of the allowed domains - for (const allowedDomain of validatedUrls) { + for (const allowedDomain of urlsToCheck) { if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; - - // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { const productId = 'X6EcdUZFY'; const environmentId = '2lwIjczu1'; - const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', headers: { @@ -62,29 +60,28 @@ function thriveStackPlugin(pluginConfig = {}) { }, body: JSON.stringify({ query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + value } } } - }`, + } + }`, variables: { input: { productId: productId, - environmentId: environmentId, - stepId: stepId + environmentId: environmentId } } }) @@ -93,55 +90,60 @@ function thriveStackPlugin(pluginConfig = {}) { console.log("API Response:", result); if (result.data && result.data.getCAOnboardingStepDetails) { const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - - // Find the name_your_website step + let marketingUrls = []; + let productUrls = []; for (const module of moduleDetails) { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); - const currentHost = window.location.hostname; - const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); - if (isValid) { - console.log('Website domain validated successfully'); - return true; - } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); - return false; + if (module.moduleId === 'marketing_attribution') { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing URLs JSON:', parseError); + } + } + } + } + if (module.moduleId === 'product_analytics') { + for (const step of module.stepDetails) { + for (const subStep of step.subStepDetails || []) { + if (subStep.subStepId === 'product_url' && subStep.value) { + try { + const subStepValue = JSON.parse(subStep.value); + if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { + productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product URLs JSON:', parseError); } - } else { - console.warn('No website_urls array found in step value'); - return false; } - } catch (parseError) { - console.error('Failed to parse step value JSON:', parseError); - return false; } } } } + localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); + localStorage.setItem('product_urls', JSON.stringify(productUrls)); + localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); + console.log('Stored product URLs:', productUrls); + console.log('Stored marketing URLs:', marketingUrls); + return true; } - console.warn('No name_your_website step found in API response'); + console.warn('No valid website URLs found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); return false; } }; - - // Load ThriveStack script function const loadThriveStackScript = () => { return new Promise((resolve, reject) => { - // Check if script is already loaded if (window.ThriveStack) { resolve(window.ThriveStack); return; } - - // Create script element const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; @@ -161,8 +163,6 @@ function thriveStackPlugin(pluginConfig = {}) { if (options.respectDoNotTrack !== undefined) { script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); } - - // Set up onload handler script.onload = () => { if (window.ThriveStack) { if (options.debug) { @@ -173,18 +173,12 @@ function thriveStackPlugin(pluginConfig = {}) { reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); } }; - - // Set up error handler script.onerror = () => { reject(new Error('Failed to load ThriveStack script')); }; - - // Append script to head document.head.appendChild(script); }); }; - - // Process the event queue const processQueue = () => { if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { return; @@ -200,8 +194,6 @@ function thriveStackPlugin(pluginConfig = {}) { }); window._tsq = []; }; - - // Create plugin object const plugin = { name: 'thrivestack', config: { @@ -216,10 +208,7 @@ function thriveStackPlugin(pluginConfig = {}) { try { await loadThriveStackScript(); console.log('ThriveStack script loaded successfully'); - - // Initialize ThriveStack if (window.ThriveStack) { - // If ThriveStack's init method takes parameters, pass them if (config.userId) { await window.ThriveStack.init(config.userId, options.source || ''); } else { @@ -227,25 +216,19 @@ function thriveStackPlugin(pluginConfig = {}) { } console.log('ThriveStack plugin initialized'); } - - // Validate website domain const isValid = await validateWebsite(); if (!isValid) { console.warn('Website domain validation failed. Analytics calls will be blocked.'); } - - // Set initialization complete initComplete(analyticsInstance); initCompleted = true; - - // Process queued events processQueue(); } catch (error) { console.error('Failed to initialize ThriveStack plugin:', error); throw error; } }, - // Core methods + // CORE-METHODS identify: ({ payload @@ -261,8 +244,9 @@ function thriveStackPlugin(pluginConfig = {}) { traits = {}, options = {} } = payload; - if (!isValidDomain()) { - console.warn('Website domain not validated. Identify call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -317,18 +301,14 @@ function thriveStackPlugin(pluginConfig = {}) { properties = {}, options = {} } = payload; - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Track call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get the IDs without fallback to empty string const deviceId = window.ThriveStack.getDeviceId(); const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: userId or deviceId must be present 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); @@ -344,11 +324,9 @@ function thriveStackPlugin(pluginConfig = {}) { event_name: event, properties: properties, user_id: userId || '', - // Use empty string only when sending the payload context: { group_id: groupId, device_id: deviceId || '', - // Use empty string only when sending the payload session_id: sessionId, source: source }, @@ -371,22 +349,21 @@ function thriveStackPlugin(pluginConfig = {}) { }]); return; } - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Page call blocked.'); + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { - // ThriveStack has its own page tracking, so we can tap into that - // or explicitly trigger a page event window.ThriveStack.capturePageVisit(); } catch (err) { console.error('Failed to send page event:', err); } }, - // Additional methods based on Analytics package spec - reset: (payload, next) => { if (!window.ThriveStack) { if (next) next(payload); @@ -395,8 +372,6 @@ function thriveStackPlugin(pluginConfig = {}) { try { window.ThriveStack.setUserId(''); window.ThriveStack.setGroupId(''); - - // Clear cookies if necessary 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`; @@ -410,10 +385,8 @@ function thriveStackPlugin(pluginConfig = {}) { } }, ready: payload => { - // Called when analytics instance is fully initialized return initCompleted; }, - // Storage methods storage: { getItem: key => { try { @@ -442,14 +415,16 @@ function thriveStackPlugin(pluginConfig = {}) { } } }, - // Context methods setAnonymousId: anonymousId => { if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } try { - // ThriveStack uses device ID as anonymous ID const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - // ThriveStack doesn't allow setting device ID externally console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; @@ -458,18 +433,20 @@ function thriveStackPlugin(pluginConfig = {}) { return false; } }, - // User methods user: () => { if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`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 }; }, - // Plugin-specific methods methods: { - // Group identification() group: (groupId, traits = {}, options = {}, callback) => { if (!window.ThriveStack) { const error = new Error('ThriveStack not initialized'); @@ -478,10 +455,11 @@ function thriveStackPlugin(pluginConfig = {}) { } return Promise.reject(error); } + const source = options.source || window.ThriveStack.getSource() || ''; // Check domain validation before processing - if (!isValidDomain()) { - const error = new Error('Website domain not validated. Group call blocked.'); + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); console.warn(error.message); if (callback && typeof callback === 'function') { callback(error); @@ -537,6 +515,11 @@ function thriveStackPlugin(pluginConfig = {}) { }); }, setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } if (config.apiKey) { API_KEY = config.apiKey; } @@ -546,8 +529,9 @@ function thriveStackPlugin(pluginConfig = {}) { return true; }, setConsent: (category, enabled) => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. setConsent call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -628,8 +612,9 @@ function thriveStackPlugin(pluginConfig = {}) { }, // Page tracking methods capturePageVisit: () => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. capturePageVisit call blocked.'); + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/lib/index.es.js b/packages/analytics-plugin-thrivestack/lib/index.es.js index 65f5a4ee..0f40f3ef 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -21,37 +21,35 @@ function thriveStackPlugin(pluginConfig = {}) { initCompleted = true; console.log('ThriveStack initialization completed'); }; - - // Helper function to normalize URLs by removing protocol and anything after the first slash const normalizeUrl = url => { return url.replace(/^https?:\/\//, '').split('/')[0]; }; - - // Helper function to check if current domain matches any validated domain - const isValidDomain = () => { - const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); - if (!validatedUrls.length) { - console.warn('No validated website URLs found in localStorage.'); + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = JSON.parse(localStorage.getItem('product_urls') || '[]'); + } else if (source === 'marketing') { + urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); + } else { + urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + } + if (!urlsToCheck.length) { + console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); return false; } const currentHost = window.location.hostname; - - // Check if current host matches any of the allowed domains - for (const allowedDomain of validatedUrls) { + for (const allowedDomain of urlsToCheck) { if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; - - // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { const productId = 'X6EcdUZFY'; const environmentId = '2lwIjczu1'; - const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', headers: { @@ -60,29 +58,28 @@ function thriveStackPlugin(pluginConfig = {}) { }, body: JSON.stringify({ query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + value } } } - }`, + } + }`, variables: { input: { productId: productId, - environmentId: environmentId, - stepId: stepId + environmentId: environmentId } } }) @@ -91,55 +88,60 @@ function thriveStackPlugin(pluginConfig = {}) { console.log("API Response:", result); if (result.data && result.data.getCAOnboardingStepDetails) { const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - - // Find the name_your_website step + let marketingUrls = []; + let productUrls = []; for (const module of moduleDetails) { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); - const currentHost = window.location.hostname; - const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); - if (isValid) { - console.log('Website domain validated successfully'); - return true; - } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); - return false; + if (module.moduleId === 'marketing_attribution') { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing URLs JSON:', parseError); + } + } + } + } + if (module.moduleId === 'product_analytics') { + for (const step of module.stepDetails) { + for (const subStep of step.subStepDetails || []) { + if (subStep.subStepId === 'product_url' && subStep.value) { + try { + const subStepValue = JSON.parse(subStep.value); + if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { + productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product URLs JSON:', parseError); } - } else { - console.warn('No website_urls array found in step value'); - return false; } - } catch (parseError) { - console.error('Failed to parse step value JSON:', parseError); - return false; } } } } + localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); + localStorage.setItem('product_urls', JSON.stringify(productUrls)); + localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); + console.log('Stored product URLs:', productUrls); + console.log('Stored marketing URLs:', marketingUrls); + return true; } - console.warn('No name_your_website step found in API response'); + console.warn('No valid website URLs found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); return false; } }; - - // Load ThriveStack script function const loadThriveStackScript = () => { return new Promise((resolve, reject) => { - // Check if script is already loaded if (window.ThriveStack) { resolve(window.ThriveStack); return; } - - // Create script element const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; @@ -159,8 +161,6 @@ function thriveStackPlugin(pluginConfig = {}) { if (options.respectDoNotTrack !== undefined) { script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); } - - // Set up onload handler script.onload = () => { if (window.ThriveStack) { if (options.debug) { @@ -171,18 +171,12 @@ function thriveStackPlugin(pluginConfig = {}) { reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); } }; - - // Set up error handler script.onerror = () => { reject(new Error('Failed to load ThriveStack script')); }; - - // Append script to head document.head.appendChild(script); }); }; - - // Process the event queue const processQueue = () => { if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { return; @@ -198,8 +192,6 @@ function thriveStackPlugin(pluginConfig = {}) { }); window._tsq = []; }; - - // Create plugin object const plugin = { name: 'thrivestack', config: { @@ -214,10 +206,7 @@ function thriveStackPlugin(pluginConfig = {}) { try { await loadThriveStackScript(); console.log('ThriveStack script loaded successfully'); - - // Initialize ThriveStack if (window.ThriveStack) { - // If ThriveStack's init method takes parameters, pass them if (config.userId) { await window.ThriveStack.init(config.userId, options.source || ''); } else { @@ -225,25 +214,19 @@ function thriveStackPlugin(pluginConfig = {}) { } console.log('ThriveStack plugin initialized'); } - - // Validate website domain const isValid = await validateWebsite(); if (!isValid) { console.warn('Website domain validation failed. Analytics calls will be blocked.'); } - - // Set initialization complete initComplete(analyticsInstance); initCompleted = true; - - // Process queued events processQueue(); } catch (error) { console.error('Failed to initialize ThriveStack plugin:', error); throw error; } }, - // Core methods + // CORE-METHODS identify: ({ payload @@ -259,8 +242,9 @@ function thriveStackPlugin(pluginConfig = {}) { traits = {}, options = {} } = payload; - if (!isValidDomain()) { - console.warn('Website domain not validated. Identify call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -315,18 +299,14 @@ function thriveStackPlugin(pluginConfig = {}) { properties = {}, options = {} } = payload; - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Track call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get the IDs without fallback to empty string const deviceId = window.ThriveStack.getDeviceId(); const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: userId or deviceId must be present 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); @@ -342,11 +322,9 @@ function thriveStackPlugin(pluginConfig = {}) { event_name: event, properties: properties, user_id: userId || '', - // Use empty string only when sending the payload context: { group_id: groupId, device_id: deviceId || '', - // Use empty string only when sending the payload session_id: sessionId, source: source }, @@ -369,22 +347,21 @@ function thriveStackPlugin(pluginConfig = {}) { }]); return; } - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Page call blocked.'); + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { - // ThriveStack has its own page tracking, so we can tap into that - // or explicitly trigger a page event window.ThriveStack.capturePageVisit(); } catch (err) { console.error('Failed to send page event:', err); } }, - // Additional methods based on Analytics package spec - reset: (payload, next) => { if (!window.ThriveStack) { if (next) next(payload); @@ -393,8 +370,6 @@ function thriveStackPlugin(pluginConfig = {}) { try { window.ThriveStack.setUserId(''); window.ThriveStack.setGroupId(''); - - // Clear cookies if necessary 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`; @@ -408,10 +383,8 @@ function thriveStackPlugin(pluginConfig = {}) { } }, ready: payload => { - // Called when analytics instance is fully initialized return initCompleted; }, - // Storage methods storage: { getItem: key => { try { @@ -440,14 +413,16 @@ function thriveStackPlugin(pluginConfig = {}) { } } }, - // Context methods setAnonymousId: anonymousId => { if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } try { - // ThriveStack uses device ID as anonymous ID const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - // ThriveStack doesn't allow setting device ID externally console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; @@ -456,18 +431,20 @@ function thriveStackPlugin(pluginConfig = {}) { return false; } }, - // User methods user: () => { if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`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 }; }, - // Plugin-specific methods methods: { - // Group identification() group: (groupId, traits = {}, options = {}, callback) => { if (!window.ThriveStack) { const error = new Error('ThriveStack not initialized'); @@ -476,10 +453,11 @@ function thriveStackPlugin(pluginConfig = {}) { } return Promise.reject(error); } + const source = options.source || window.ThriveStack.getSource() || ''; // Check domain validation before processing - if (!isValidDomain()) { - const error = new Error('Website domain not validated. Group call blocked.'); + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); console.warn(error.message); if (callback && typeof callback === 'function') { callback(error); @@ -535,6 +513,11 @@ function thriveStackPlugin(pluginConfig = {}) { }); }, setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } if (config.apiKey) { API_KEY = config.apiKey; } @@ -544,8 +527,9 @@ function thriveStackPlugin(pluginConfig = {}) { return true; }, setConsent: (category, enabled) => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. setConsent call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -626,8 +610,9 @@ function thriveStackPlugin(pluginConfig = {}) { }, // Page tracking methods capturePageVisit: () => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. capturePageVisit call blocked.'); + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/lib/index.js b/packages/analytics-plugin-thrivestack/lib/index.js index 2a139a53..28097e20 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.js +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -23,37 +23,35 @@ function thriveStackPlugin(pluginConfig = {}) { initCompleted = true; console.log('ThriveStack initialization completed'); }; - - // Helper function to normalize URLs by removing protocol and anything after the first slash const normalizeUrl = url => { return url.replace(/^https?:\/\//, '').split('/')[0]; }; - - // Helper function to check if current domain matches any validated domain - const isValidDomain = () => { - const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); - if (!validatedUrls.length) { - console.warn('No validated website URLs found in localStorage.'); + const isValidDomain = (source = '') => { + let urlsToCheck = []; + if (source === 'product') { + urlsToCheck = JSON.parse(localStorage.getItem('product_urls') || '[]'); + } else if (source === 'marketing') { + urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); + } else { + urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + } + if (!urlsToCheck.length) { + console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); return false; } const currentHost = window.location.hostname; - - // Check if current host matches any of the allowed domains - for (const allowedDomain of validatedUrls) { + for (const allowedDomain of urlsToCheck) { if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; - - // Fetch onboarding step details and validate website domain const validateWebsite = async () => { try { const productId = 'X6EcdUZFY'; const environmentId = '2lwIjczu1'; - const stepId = 'name_your_website'; const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', headers: { @@ -62,29 +60,28 @@ function thriveStackPlugin(pluginConfig = {}) { }, body: JSON.stringify({ query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + value } } } - }`, + } + }`, variables: { input: { productId: productId, - environmentId: environmentId, - stepId: stepId + environmentId: environmentId } } }) @@ -93,55 +90,60 @@ function thriveStackPlugin(pluginConfig = {}) { console.log("API Response:", result); if (result.data && result.data.getCAOnboardingStepDetails) { const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - - // Find the name_your_website step + let marketingUrls = []; + let productUrls = []; for (const module of moduleDetails) { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); - const currentHost = window.location.hostname; - const isValid = normalizedUrls.some(domain => currentHost.includes(domain) || domain.includes(currentHost)); - if (isValid) { - console.log('Website domain validated successfully'); - return true; - } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); - return false; + if (module.moduleId === 'marketing_attribution') { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing URLs JSON:', parseError); + } + } + } + } + if (module.moduleId === 'product_analytics') { + for (const step of module.stepDetails) { + for (const subStep of step.subStepDetails || []) { + if (subStep.subStepId === 'product_url' && subStep.value) { + try { + const subStepValue = JSON.parse(subStep.value); + if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { + productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product URLs JSON:', parseError); } - } else { - console.warn('No website_urls array found in step value'); - return false; } - } catch (parseError) { - console.error('Failed to parse step value JSON:', parseError); - return false; } } } } + localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); + localStorage.setItem('product_urls', JSON.stringify(productUrls)); + localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); + console.log('Stored product URLs:', productUrls); + console.log('Stored marketing URLs:', marketingUrls); + return true; } - console.warn('No name_your_website step found in API response'); + console.warn('No valid website URLs found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); return false; } }; - - // Load ThriveStack script function const loadThriveStackScript = () => { return new Promise((resolve, reject) => { - // Check if script is already loaded if (window.ThriveStack) { resolve(window.ThriveStack); return; } - - // Create script element const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; @@ -161,8 +163,6 @@ function thriveStackPlugin(pluginConfig = {}) { if (options.respectDoNotTrack !== undefined) { script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); } - - // Set up onload handler script.onload = () => { if (window.ThriveStack) { if (options.debug) { @@ -173,18 +173,12 @@ function thriveStackPlugin(pluginConfig = {}) { reject(new Error('ThriveStack script loaded but window.ThriveStack is not defined')); } }; - - // Set up error handler script.onerror = () => { reject(new Error('Failed to load ThriveStack script')); }; - - // Append script to head document.head.appendChild(script); }); }; - - // Process the event queue const processQueue = () => { if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { return; @@ -200,8 +194,6 @@ function thriveStackPlugin(pluginConfig = {}) { }); window._tsq = []; }; - - // Create plugin object const plugin = { name: 'thrivestack', config: { @@ -216,10 +208,7 @@ function thriveStackPlugin(pluginConfig = {}) { try { await loadThriveStackScript(); console.log('ThriveStack script loaded successfully'); - - // Initialize ThriveStack if (window.ThriveStack) { - // If ThriveStack's init method takes parameters, pass them if (config.userId) { await window.ThriveStack.init(config.userId, options.source || ''); } else { @@ -227,25 +216,19 @@ function thriveStackPlugin(pluginConfig = {}) { } console.log('ThriveStack plugin initialized'); } - - // Validate website domain const isValid = await validateWebsite(); if (!isValid) { console.warn('Website domain validation failed. Analytics calls will be blocked.'); } - - // Set initialization complete initComplete(analyticsInstance); initCompleted = true; - - // Process queued events processQueue(); } catch (error) { console.error('Failed to initialize ThriveStack plugin:', error); throw error; } }, - // Core methods + // CORE-METHODS identify: ({ payload @@ -261,8 +244,9 @@ function thriveStackPlugin(pluginConfig = {}) { traits = {}, options = {} } = payload; - if (!isValidDomain()) { - console.warn('Website domain not validated. Identify call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -317,18 +301,14 @@ function thriveStackPlugin(pluginConfig = {}) { properties = {}, options = {} } = payload; - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Track call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get the IDs without fallback to empty string const deviceId = window.ThriveStack.getDeviceId(); const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: userId or deviceId must be present 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); @@ -344,11 +324,9 @@ function thriveStackPlugin(pluginConfig = {}) { event_name: event, properties: properties, user_id: userId || '', - // Use empty string only when sending the payload context: { group_id: groupId, device_id: deviceId || '', - // Use empty string only when sending the payload session_id: sessionId, source: source }, @@ -371,22 +349,21 @@ function thriveStackPlugin(pluginConfig = {}) { }]); return; } - - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Page call blocked.'); + const { + properties = {}, + options = {} + } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { - // ThriveStack has its own page tracking, so we can tap into that - // or explicitly trigger a page event window.ThriveStack.capturePageVisit(); } catch (err) { console.error('Failed to send page event:', err); } }, - // Additional methods based on Analytics package spec - reset: (payload, next) => { if (!window.ThriveStack) { if (next) next(payload); @@ -395,8 +372,6 @@ function thriveStackPlugin(pluginConfig = {}) { try { window.ThriveStack.setUserId(''); window.ThriveStack.setGroupId(''); - - // Clear cookies if necessary 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`; @@ -410,10 +385,8 @@ function thriveStackPlugin(pluginConfig = {}) { } }, ready: payload => { - // Called when analytics instance is fully initialized return initCompleted; }, - // Storage methods storage: { getItem: key => { try { @@ -442,14 +415,16 @@ function thriveStackPlugin(pluginConfig = {}) { } } }, - // Context methods setAnonymousId: anonymousId => { if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } try { - // ThriveStack uses device ID as anonymous ID const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - // ThriveStack doesn't allow setting device ID externally console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; @@ -458,18 +433,20 @@ function thriveStackPlugin(pluginConfig = {}) { return false; } }, - // User methods user: () => { if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`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 }; }, - // Plugin-specific methods methods: { - // Group identification() group: (groupId, traits = {}, options = {}, callback) => { if (!window.ThriveStack) { const error = new Error('ThriveStack not initialized'); @@ -478,10 +455,11 @@ function thriveStackPlugin(pluginConfig = {}) { } return Promise.reject(error); } + const source = options.source || window.ThriveStack.getSource() || ''; // Check domain validation before processing - if (!isValidDomain()) { - const error = new Error('Website domain not validated. Group call blocked.'); + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); console.warn(error.message); if (callback && typeof callback === 'function') { callback(error); @@ -537,6 +515,11 @@ function thriveStackPlugin(pluginConfig = {}) { }); }, setApiConfig: (config = {}) => { + const source = options.source || window.ThriveStack.getSource() || ''; + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } if (config.apiKey) { API_KEY = config.apiKey; } @@ -546,8 +529,9 @@ function thriveStackPlugin(pluginConfig = {}) { return true; }, setConsent: (category, enabled) => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. setConsent call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -628,8 +612,9 @@ function thriveStackPlugin(pluginConfig = {}) { }, // Page tracking methods capturePageVisit: () => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. capturePageVisit call blocked.'); + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/src/index.js b/packages/analytics-plugin-thrivestack/src/index.js index b8a99aed..ccbb435a 100644 --- a/packages/analytics-plugin-thrivestack/src/index.js +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -27,39 +27,44 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack initialization completed'); }; - // Helper function to normalize URLs by removing protocol and anything after the first slash const normalizeUrl = (url) => { return url.replace(/^https?:\/\//, '').split('/')[0]; }; - // Helper function to check if current domain matches any validated domain - const isValidDomain = () => { - const validatedUrls = JSON.parse(localStorage.getItem('website_urls') || '[]'); - if (!validatedUrls.length) { - console.warn('No validated website URLs found in localStorage.'); + const isValidDomain = (source = '') => { + let urlsToCheck = []; + + if (source === 'product') { + urlsToCheck = JSON.parse(localStorage.getItem('product_urls') || '[]'); + } else if (source === 'marketing') { + urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); + } else { + urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + } + + if (!urlsToCheck.length) { + console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); return false; } const currentHost = window.location.hostname; - - // Check if current host matches any of the allowed domains - for (const allowedDomain of validatedUrls) { + + for (const allowedDomain of urlsToCheck) { if (currentHost.includes(allowedDomain) || allowedDomain.includes(currentHost)) { return true; } } - - console.warn(`Current host ${currentHost} does not match any validated domains: ${validatedUrls.join(', ')}`); + + console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; - // Fetch onboarding step details and validate website domain + const validateWebsite = async () => { - try { - const productId = 'X6EcdUZFY'; - const environmentId = '2lwIjczu1'; - const stepId = 'name_your_website'; - + try { + const productId = 'X6EcdUZFY'; + const environmentId = '2lwIjczu1'; + const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { method: 'POST', headers: { @@ -68,78 +73,88 @@ function thriveStackPlugin(pluginConfig = {}) { }, body: JSON.stringify({ query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId + getCAOnboardingStepDetails(input: $input) { + productId + environmentId + moduleDetails { + moduleId + stepDetails { + stepId + isCompleted + value + subStepDetails { + subStepId isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + value } } } - }`, + } + }`, variables: { input: { productId: productId, - environmentId: environmentId, - stepId: stepId + environmentId: environmentId } } }) }); - + const result = await response.json(); console.log("API Response:", result); - + if (result.data && result.data.getCAOnboardingStepDetails) { const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - - // Find the name_your_website step + let marketingUrls = []; + let productUrls = []; + for (const module of moduleDetails) { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - const normalizedUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - localStorage.setItem('website_urls', JSON.stringify(normalizedUrls)); - - const currentHost = window.location.hostname; - - const isValid = normalizedUrls.some(domain => - currentHost.includes(domain) || domain.includes(currentHost) - ); - - if (isValid) { - console.log('Website domain validated successfully'); - return true; - } else { - console.log(`Domain mismatch: Current=${currentHost}, Allowed=${normalizedUrls.join(', ')}`); - return false; + + if (module.moduleId === 'marketing_attribution') { + for (const step of module.stepDetails) { + if (step.stepId === 'name_your_website' && step.value) { + try { + const stepValue = JSON.parse(step.value); + if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { + marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse marketing URLs JSON:', parseError); + } + } + } + } + + if (module.moduleId === 'product_analytics') { + for (const step of module.stepDetails) { + for (const subStep of step.subStepDetails || []) { + if (subStep.subStepId === 'product_url' && subStep.value) { + try { + const subStepValue = JSON.parse(subStep.value); + if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { + productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); + } + } catch (parseError) { + console.error('Failed to parse product URLs JSON:', parseError); } - } else { - console.warn('No website_urls array found in step value'); - return false; } - } catch (parseError) { - console.error('Failed to parse step value JSON:', parseError); - return false; } } } } + + + localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); + localStorage.setItem('product_urls', JSON.stringify(productUrls)); + localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); + + console.log('Stored product URLs:', productUrls); + console.log('Stored marketing URLs:', marketingUrls); + + return true; } - - console.warn('No name_your_website step found in API response'); + + console.warn('No valid website URLs found in API response'); return false; } catch (error) { console.error('Failed to validate website:', error); @@ -147,16 +162,14 @@ function thriveStackPlugin(pluginConfig = {}) { } }; - // Load ThriveStack script function + const loadThriveStackScript = () => { return new Promise((resolve, reject) => { - // Check if script is already loaded if (window.ThriveStack) { resolve(window.ThriveStack); return; } - // Create script element const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; @@ -182,7 +195,6 @@ function thriveStackPlugin(pluginConfig = {}) { script.setAttribute('respect-dnt', options.respectDoNotTrack ? 'true' : 'false'); } - // Set up onload handler script.onload = () => { if (window.ThriveStack) { if (options.debug) { @@ -194,17 +206,14 @@ function thriveStackPlugin(pluginConfig = {}) { } }; - // Set up error handler script.onerror = () => { reject(new Error('Failed to load ThriveStack script')); }; - // Append script to head document.head.appendChild(script); }); }; - // Process the event queue const processQueue = () => { if (!window.ThriveStack || !Array.isArray(window._tsq) || window._tsq.length === 0) { return; @@ -223,7 +232,6 @@ function thriveStackPlugin(pluginConfig = {}) { window._tsq = []; }; - // Create plugin object const plugin = { name: 'thrivestack', config: { @@ -239,9 +247,7 @@ function thriveStackPlugin(pluginConfig = {}) { await loadThriveStackScript(); console.log('ThriveStack script loaded successfully'); - // Initialize ThriveStack if (window.ThriveStack) { - // If ThriveStack's init method takes parameters, pass them if (config.userId) { await window.ThriveStack.init(config.userId, options.source || ''); } else { @@ -251,18 +257,15 @@ function thriveStackPlugin(pluginConfig = {}) { console.log('ThriveStack plugin initialized'); } - // Validate website domain const isValid = await validateWebsite(); - + if (!isValid) { console.warn('Website domain validation failed. Analytics calls will be blocked.'); } - // Set initialization complete initComplete(analyticsInstance); initCompleted = true; - // Process queued events processQueue(); } catch (error) { console.error('Failed to initialize ThriveStack plugin:', error); @@ -270,18 +273,18 @@ function thriveStackPlugin(pluginConfig = {}) { } }, - // Core methods + // 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()) { - console.warn('Website domain not validated. Identify call blocked.'); + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } @@ -334,19 +337,19 @@ function thriveStackPlugin(pluginConfig = {}) { } const { event, properties = {}, options = {} } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Track call blocked.'); + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get the IDs without fallback to empty string + const deviceId = window.ThriveStack.getDeviceId(); const userId = options.userId || options.user_id || window.ThriveStack.userId; - // Validation: userId or deviceId must be present + 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); @@ -363,10 +366,10 @@ function thriveStackPlugin(pluginConfig = {}) { const eventPayload = [{ event_name: event, properties: properties, - user_id: userId || '', // Use empty string only when sending the payload + user_id: userId || '', context: { group_id: groupId, - device_id: deviceId || '', // Use empty string only when sending the payload + device_id: deviceId || '', session_id: sessionId, source: source }, @@ -389,23 +392,23 @@ function thriveStackPlugin(pluginConfig = {}) { } const { properties = {}, options = {} } = payload; + const source = options.source || window.ThriveStack.getSource() || ''; - // Check domain validation before processing - if (!isValidDomain()) { - console.warn('Website domain not validated. Page call blocked.'); + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { - // ThriveStack has its own page tracking, so we can tap into that - // or explicitly trigger a page event + + window.ThriveStack.capturePageVisit(); } catch (err) { console.error('Failed to send page event:', err); } }, - // Additional methods based on Analytics package spec + reset: (payload, next) => { if (!window.ThriveStack) { @@ -417,7 +420,7 @@ function thriveStackPlugin(pluginConfig = {}) { window.ThriveStack.setUserId('') window.ThriveStack.setGroupId('') - // Clear cookies if necessary + 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`; @@ -433,11 +436,11 @@ function thriveStackPlugin(pluginConfig = {}) { }, ready: (payload) => { - // Called when analytics instance is fully initialized + return initCompleted; }, - // Storage methods + storage: { getItem: (key) => { try { @@ -469,15 +472,22 @@ function thriveStackPlugin(pluginConfig = {}) { } }, - // Context methods + setAnonymousId: (anonymousId) => { if (!window.ThriveStack) return false; + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + return; + } + try { - // ThriveStack uses device ID as anonymous ID + const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - // ThriveStack doesn't allow setting device ID externally + console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; @@ -487,10 +497,17 @@ function thriveStackPlugin(pluginConfig = {}) { } }, - // User methods + user: () => { if (!window.ThriveStack) return null; + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. User call blocked.`); + return; + } + return { id: window.ThriveStack.userId || null, anonymousId: window.ThriveStack.getDeviceId() || null, @@ -498,9 +515,9 @@ function thriveStackPlugin(pluginConfig = {}) { }; }, - // Plugin-specific methods + methods: { - // Group identification() + group: (groupId, traits = {}, options = {}, callback) => { if (!window.ThriveStack) { const error = new Error('ThriveStack not initialized'); @@ -510,9 +527,11 @@ function thriveStackPlugin(pluginConfig = {}) { return Promise.reject(error); } + const source = options.source || window.ThriveStack.getSource() || ''; + // Check domain validation before processing - if (!isValidDomain()) { - const error = new Error('Website domain not validated. Group call blocked.'); + if (!isValidDomain(source)) { + const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); console.warn(error.message); if (callback && typeof callback === 'function') { callback(error); @@ -575,6 +594,14 @@ function thriveStackPlugin(pluginConfig = {}) { }, setApiConfig: (config = {}) => { + + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + return; + } + if (config.apiKey) { API_KEY = config.apiKey; } @@ -587,8 +614,10 @@ function thriveStackPlugin(pluginConfig = {}) { }, setConsent: (category, enabled) => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. setConsent call blocked.'); + const source = options.source || window.ThriveStack.getSource() || ''; + + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } @@ -685,8 +714,9 @@ function thriveStackPlugin(pluginConfig = {}) { // Page tracking methods capturePageVisit: () => { - if (!window.ThriveStack || !isValidDomain()) { - console.warn('Website domain not validated. capturePageVisit call blocked.'); + const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; + if (!window.ThriveStack || !isValidDomain(source)) { + console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } From 9143384bf1e0fdc9067ded9f7e4002f9f95acdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Fri, 9 May 2025 16:11:51 +0530 Subject: [PATCH 4/8] update: error handling for non-registered domains --- .../lib/index.browser.es.js | 235 ++++++++------ .../lib/index.browser.js | 235 ++++++++------ .../lib/index.es.js | 235 ++++++++------ .../analytics-plugin-thrivestack/lib/index.js | 235 ++++++++------ .../analytics-plugin-thrivestack/src/index.js | 291 ++++++++++-------- 5 files changed, 714 insertions(+), 517 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js index 0f40f3ef..f4769a03 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -24,17 +24,39 @@ function thriveStackPlugin(pluginConfig = {}) { 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 = JSON.parse(localStorage.getItem('product_urls') || '[]'); + urlsToCheck = getCookie('product_urls'); } else if (source === 'marketing') { - urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); - } else { - urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + urlsToCheck = getCookie('marketing_urls'); } if (!urlsToCheck.length) { - console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); return false; } const currentHost = window.location.hostname; @@ -43,99 +65,121 @@ function thriveStackPlugin(pluginConfig = {}) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; const validateWebsite = async () => { try { - const productId = 'X6EcdUZFY'; - const environmentId = '2lwIjczu1'; - const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'x-api-key': pluginConfig.apiKey }, body: JSON.stringify({ - query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId - isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.dev.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); + 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)); } } } - }`, - variables: { - input: { - productId: productId, - environmentId: environmentId + } 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); } - }) - }); - const result = await response.json(); - console.log("API Response:", result); - if (result.data && result.data.getCAOnboardingStepDetails) { - const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - let marketingUrls = []; - let productUrls = []; - for (const module of moduleDetails) { - if (module.moduleId === 'marketing_attribution') { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse marketing URLs JSON:', 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)); } } } - if (module.moduleId === 'product_analytics') { - for (const step of module.stepDetails) { - for (const subStep of step.subStepDetails || []) { - if (subStep.subStepId === 'product_url' && subStep.value) { - try { - const subStepValue = JSON.parse(subStep.value); - if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { - productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse product URLs JSON:', parseError); - } - } - } + } 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); } } - localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); - localStorage.setItem('product_urls', JSON.stringify(productUrls)); - localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); - console.log('Stored product URLs:', productUrls); - console.log('Stored marketing URLs:', marketingUrls); - return true; } - console.warn('No valid website URLs found in API response'); - return false; + 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) { @@ -216,7 +260,7 @@ function thriveStackPlugin(pluginConfig = {}) { } const isValid = await validateWebsite(); if (!isValid) { - console.warn('Website domain validation failed. Analytics calls will be blocked.'); + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); } initComplete(analyticsInstance); initCompleted = true; @@ -244,14 +288,11 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get deviceId without fallback const deviceId = window.ThriveStack.getDeviceId(); - - // Validation: deviceId must be present if (options.source === 'marketing' && !deviceId) { const error = new Error('Identify call requires deviceId to be present'); console.error('Failed to send identify event:', error); @@ -262,7 +303,6 @@ function thriveStackPlugin(pluginConfig = {}) { } const identifyPayload = [{ user_id: userId || '', - // Use empty string only when sending the payload traits: traits, timestamp: options.timestamp || new Date().toISOString(), context: { @@ -301,7 +341,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -353,7 +393,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { @@ -373,6 +413,8 @@ function thriveStackPlugin(pluginConfig = {}) { 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; @@ -388,7 +430,7 @@ function thriveStackPlugin(pluginConfig = {}) { storage: { getItem: key => { try { - return localStorage.getItem(`thrivestack_${key}`); + return getCookie(`thrivestack_${key}`); } catch (err) { console.error('Failed to get item from storage:', err); return null; @@ -396,7 +438,7 @@ function thriveStackPlugin(pluginConfig = {}) { }, setItem: (key, value) => { try { - localStorage.setItem(`thrivestack_${key}`, value); + setCookie(`thrivestack_${key}`, value); return true; } catch (err) { console.error('Failed to set item in storage:', err); @@ -405,7 +447,8 @@ function thriveStackPlugin(pluginConfig = {}) { }, removeItem: key => { try { - localStorage.removeItem(`thrivestack_${key}`); + 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); @@ -417,13 +460,13 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return false; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); return; } try { const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; } catch (err) { @@ -435,7 +478,7 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return null; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. User call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); return; } return { @@ -454,24 +497,18 @@ function thriveStackPlugin(pluginConfig = {}) { return Promise.reject(error); } const source = options.source || window.ThriveStack.getSource() || ''; - - // Check domain validation before processing if (!isValidDomain(source)) { const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); - console.warn(error.message); + showValidationError(error.message); if (callback && typeof callback === 'function') { callback(error); } return Promise.reject(error); } - - // Get userId without fallback const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: both userId and groupId must be present if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -479,7 +516,7 @@ function thriveStackPlugin(pluginConfig = {}) { } if (!groupId) { const error = new Error('Group identify requires groupId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -505,7 +542,7 @@ function thriveStackPlugin(pluginConfig = {}) { } return result; }).catch(err => { - console.error('Failed to send group event:', err); + showValidationError('Failed to send group event:'); if (callback && typeof callback === 'function') { callback(err); } @@ -515,7 +552,7 @@ function thriveStackPlugin(pluginConfig = {}) { setApiConfig: (config = {}) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); return; } if (config.apiKey) { @@ -529,7 +566,7 @@ function thriveStackPlugin(pluginConfig = {}) { setConsent: (category, enabled) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -612,7 +649,7 @@ function thriveStackPlugin(pluginConfig = {}) { capturePageVisit: () => { const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.js b/packages/analytics-plugin-thrivestack/lib/index.browser.js index 28097e20..00e4abfa 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -26,17 +26,39 @@ function thriveStackPlugin(pluginConfig = {}) { 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 = JSON.parse(localStorage.getItem('product_urls') || '[]'); + urlsToCheck = getCookie('product_urls'); } else if (source === 'marketing') { - urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); - } else { - urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + urlsToCheck = getCookie('marketing_urls'); } if (!urlsToCheck.length) { - console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); return false; } const currentHost = window.location.hostname; @@ -45,99 +67,121 @@ function thriveStackPlugin(pluginConfig = {}) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; const validateWebsite = async () => { try { - const productId = 'X6EcdUZFY'; - const environmentId = '2lwIjczu1'; - const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'x-api-key': pluginConfig.apiKey }, body: JSON.stringify({ - query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId - isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.dev.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); + 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)); } } } - }`, - variables: { - input: { - productId: productId, - environmentId: environmentId + } 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); } - }) - }); - const result = await response.json(); - console.log("API Response:", result); - if (result.data && result.data.getCAOnboardingStepDetails) { - const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - let marketingUrls = []; - let productUrls = []; - for (const module of moduleDetails) { - if (module.moduleId === 'marketing_attribution') { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse marketing URLs JSON:', 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)); } } } - if (module.moduleId === 'product_analytics') { - for (const step of module.stepDetails) { - for (const subStep of step.subStepDetails || []) { - if (subStep.subStepId === 'product_url' && subStep.value) { - try { - const subStepValue = JSON.parse(subStep.value); - if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { - productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse product URLs JSON:', parseError); - } - } - } + } 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); } } - localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); - localStorage.setItem('product_urls', JSON.stringify(productUrls)); - localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); - console.log('Stored product URLs:', productUrls); - console.log('Stored marketing URLs:', marketingUrls); - return true; } - console.warn('No valid website URLs found in API response'); - return false; + 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) { @@ -218,7 +262,7 @@ function thriveStackPlugin(pluginConfig = {}) { } const isValid = await validateWebsite(); if (!isValid) { - console.warn('Website domain validation failed. Analytics calls will be blocked.'); + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); } initComplete(analyticsInstance); initCompleted = true; @@ -246,14 +290,11 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get deviceId without fallback const deviceId = window.ThriveStack.getDeviceId(); - - // Validation: deviceId must be present if (options.source === 'marketing' && !deviceId) { const error = new Error('Identify call requires deviceId to be present'); console.error('Failed to send identify event:', error); @@ -264,7 +305,6 @@ function thriveStackPlugin(pluginConfig = {}) { } const identifyPayload = [{ user_id: userId || '', - // Use empty string only when sending the payload traits: traits, timestamp: options.timestamp || new Date().toISOString(), context: { @@ -303,7 +343,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -355,7 +395,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { @@ -375,6 +415,8 @@ function thriveStackPlugin(pluginConfig = {}) { 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; @@ -390,7 +432,7 @@ function thriveStackPlugin(pluginConfig = {}) { storage: { getItem: key => { try { - return localStorage.getItem(`thrivestack_${key}`); + return getCookie(`thrivestack_${key}`); } catch (err) { console.error('Failed to get item from storage:', err); return null; @@ -398,7 +440,7 @@ function thriveStackPlugin(pluginConfig = {}) { }, setItem: (key, value) => { try { - localStorage.setItem(`thrivestack_${key}`, value); + setCookie(`thrivestack_${key}`, value); return true; } catch (err) { console.error('Failed to set item in storage:', err); @@ -407,7 +449,8 @@ function thriveStackPlugin(pluginConfig = {}) { }, removeItem: key => { try { - localStorage.removeItem(`thrivestack_${key}`); + 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); @@ -419,13 +462,13 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return false; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); return; } try { const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; } catch (err) { @@ -437,7 +480,7 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return null; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. User call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); return; } return { @@ -456,24 +499,18 @@ function thriveStackPlugin(pluginConfig = {}) { return Promise.reject(error); } const source = options.source || window.ThriveStack.getSource() || ''; - - // Check domain validation before processing if (!isValidDomain(source)) { const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); - console.warn(error.message); + showValidationError(error.message); if (callback && typeof callback === 'function') { callback(error); } return Promise.reject(error); } - - // Get userId without fallback const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: both userId and groupId must be present if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -481,7 +518,7 @@ function thriveStackPlugin(pluginConfig = {}) { } if (!groupId) { const error = new Error('Group identify requires groupId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -507,7 +544,7 @@ function thriveStackPlugin(pluginConfig = {}) { } return result; }).catch(err => { - console.error('Failed to send group event:', err); + showValidationError('Failed to send group event:'); if (callback && typeof callback === 'function') { callback(err); } @@ -517,7 +554,7 @@ function thriveStackPlugin(pluginConfig = {}) { setApiConfig: (config = {}) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); return; } if (config.apiKey) { @@ -531,7 +568,7 @@ function thriveStackPlugin(pluginConfig = {}) { setConsent: (category, enabled) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -614,7 +651,7 @@ function thriveStackPlugin(pluginConfig = {}) { capturePageVisit: () => { const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/lib/index.es.js b/packages/analytics-plugin-thrivestack/lib/index.es.js index 0f40f3ef..f4769a03 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -24,17 +24,39 @@ function thriveStackPlugin(pluginConfig = {}) { 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 = JSON.parse(localStorage.getItem('product_urls') || '[]'); + urlsToCheck = getCookie('product_urls'); } else if (source === 'marketing') { - urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); - } else { - urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + urlsToCheck = getCookie('marketing_urls'); } if (!urlsToCheck.length) { - console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); return false; } const currentHost = window.location.hostname; @@ -43,99 +65,121 @@ function thriveStackPlugin(pluginConfig = {}) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; const validateWebsite = async () => { try { - const productId = 'X6EcdUZFY'; - const environmentId = '2lwIjczu1'; - const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'x-api-key': pluginConfig.apiKey }, body: JSON.stringify({ - query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId - isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.dev.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); + 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)); } } } - }`, - variables: { - input: { - productId: productId, - environmentId: environmentId + } 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); } - }) - }); - const result = await response.json(); - console.log("API Response:", result); - if (result.data && result.data.getCAOnboardingStepDetails) { - const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - let marketingUrls = []; - let productUrls = []; - for (const module of moduleDetails) { - if (module.moduleId === 'marketing_attribution') { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse marketing URLs JSON:', 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)); } } } - if (module.moduleId === 'product_analytics') { - for (const step of module.stepDetails) { - for (const subStep of step.subStepDetails || []) { - if (subStep.subStepId === 'product_url' && subStep.value) { - try { - const subStepValue = JSON.parse(subStep.value); - if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { - productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse product URLs JSON:', parseError); - } - } - } + } 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); } } - localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); - localStorage.setItem('product_urls', JSON.stringify(productUrls)); - localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); - console.log('Stored product URLs:', productUrls); - console.log('Stored marketing URLs:', marketingUrls); - return true; } - console.warn('No valid website URLs found in API response'); - return false; + 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) { @@ -216,7 +260,7 @@ function thriveStackPlugin(pluginConfig = {}) { } const isValid = await validateWebsite(); if (!isValid) { - console.warn('Website domain validation failed. Analytics calls will be blocked.'); + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); } initComplete(analyticsInstance); initCompleted = true; @@ -244,14 +288,11 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get deviceId without fallback const deviceId = window.ThriveStack.getDeviceId(); - - // Validation: deviceId must be present if (options.source === 'marketing' && !deviceId) { const error = new Error('Identify call requires deviceId to be present'); console.error('Failed to send identify event:', error); @@ -262,7 +303,6 @@ function thriveStackPlugin(pluginConfig = {}) { } const identifyPayload = [{ user_id: userId || '', - // Use empty string only when sending the payload traits: traits, timestamp: options.timestamp || new Date().toISOString(), context: { @@ -301,7 +341,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -353,7 +393,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { @@ -373,6 +413,8 @@ function thriveStackPlugin(pluginConfig = {}) { 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; @@ -388,7 +430,7 @@ function thriveStackPlugin(pluginConfig = {}) { storage: { getItem: key => { try { - return localStorage.getItem(`thrivestack_${key}`); + return getCookie(`thrivestack_${key}`); } catch (err) { console.error('Failed to get item from storage:', err); return null; @@ -396,7 +438,7 @@ function thriveStackPlugin(pluginConfig = {}) { }, setItem: (key, value) => { try { - localStorage.setItem(`thrivestack_${key}`, value); + setCookie(`thrivestack_${key}`, value); return true; } catch (err) { console.error('Failed to set item in storage:', err); @@ -405,7 +447,8 @@ function thriveStackPlugin(pluginConfig = {}) { }, removeItem: key => { try { - localStorage.removeItem(`thrivestack_${key}`); + 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); @@ -417,13 +460,13 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return false; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); return; } try { const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; } catch (err) { @@ -435,7 +478,7 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return null; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. User call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); return; } return { @@ -454,24 +497,18 @@ function thriveStackPlugin(pluginConfig = {}) { return Promise.reject(error); } const source = options.source || window.ThriveStack.getSource() || ''; - - // Check domain validation before processing if (!isValidDomain(source)) { const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); - console.warn(error.message); + showValidationError(error.message); if (callback && typeof callback === 'function') { callback(error); } return Promise.reject(error); } - - // Get userId without fallback const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: both userId and groupId must be present if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -479,7 +516,7 @@ function thriveStackPlugin(pluginConfig = {}) { } if (!groupId) { const error = new Error('Group identify requires groupId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -505,7 +542,7 @@ function thriveStackPlugin(pluginConfig = {}) { } return result; }).catch(err => { - console.error('Failed to send group event:', err); + showValidationError('Failed to send group event:'); if (callback && typeof callback === 'function') { callback(err); } @@ -515,7 +552,7 @@ function thriveStackPlugin(pluginConfig = {}) { setApiConfig: (config = {}) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); return; } if (config.apiKey) { @@ -529,7 +566,7 @@ function thriveStackPlugin(pluginConfig = {}) { setConsent: (category, enabled) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -612,7 +649,7 @@ function thriveStackPlugin(pluginConfig = {}) { capturePageVisit: () => { const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/lib/index.js b/packages/analytics-plugin-thrivestack/lib/index.js index 28097e20..00e4abfa 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.js +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -26,17 +26,39 @@ function thriveStackPlugin(pluginConfig = {}) { 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 = JSON.parse(localStorage.getItem('product_urls') || '[]'); + urlsToCheck = getCookie('product_urls'); } else if (source === 'marketing') { - urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); - } else { - urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + urlsToCheck = getCookie('marketing_urls'); } if (!urlsToCheck.length) { - console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); return false; } const currentHost = window.location.hostname; @@ -45,99 +67,121 @@ function thriveStackPlugin(pluginConfig = {}) { return true; } } - console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; const validateWebsite = async () => { try { - const productId = 'X6EcdUZFY'; - const environmentId = '2lwIjczu1'; - const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'x-api-key': pluginConfig.apiKey }, body: JSON.stringify({ - query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId - isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + const productResponse = await fetch('https://api.dev.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); + 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)); } } } - }`, - variables: { - input: { - productId: productId, - environmentId: environmentId + } 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); } - }) - }); - const result = await response.json(); - console.log("API Response:", result); - if (result.data && result.data.getCAOnboardingStepDetails) { - const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - let marketingUrls = []; - let productUrls = []; - for (const module of moduleDetails) { - if (module.moduleId === 'marketing_attribution') { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse marketing URLs JSON:', 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)); } } } - if (module.moduleId === 'product_analytics') { - for (const step of module.stepDetails) { - for (const subStep of step.subStepDetails || []) { - if (subStep.subStepId === 'product_url' && subStep.value) { - try { - const subStepValue = JSON.parse(subStep.value); - if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { - productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse product URLs JSON:', parseError); - } - } - } + } 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); } } - localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); - localStorage.setItem('product_urls', JSON.stringify(productUrls)); - localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); - console.log('Stored product URLs:', productUrls); - console.log('Stored marketing URLs:', marketingUrls); - return true; } - console.warn('No valid website URLs found in API response'); - return false; + 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) { @@ -218,7 +262,7 @@ function thriveStackPlugin(pluginConfig = {}) { } const isValid = await validateWebsite(); if (!isValid) { - console.warn('Website domain validation failed. Analytics calls will be blocked.'); + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); } initComplete(analyticsInstance); initCompleted = true; @@ -246,14 +290,11 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get deviceId without fallback const deviceId = window.ThriveStack.getDeviceId(); - - // Validation: deviceId must be present if (options.source === 'marketing' && !deviceId) { const error = new Error('Identify call requires deviceId to be present'); console.error('Failed to send identify event:', error); @@ -264,7 +305,6 @@ function thriveStackPlugin(pluginConfig = {}) { } const identifyPayload = [{ user_id: userId || '', - // Use empty string only when sending the payload traits: traits, timestamp: options.timestamp || new Date().toISOString(), context: { @@ -303,7 +343,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { @@ -355,7 +395,7 @@ function thriveStackPlugin(pluginConfig = {}) { } = payload; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Page call blocked.`); return; } try { @@ -375,6 +415,8 @@ function thriveStackPlugin(pluginConfig = {}) { 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; @@ -390,7 +432,7 @@ function thriveStackPlugin(pluginConfig = {}) { storage: { getItem: key => { try { - return localStorage.getItem(`thrivestack_${key}`); + return getCookie(`thrivestack_${key}`); } catch (err) { console.error('Failed to get item from storage:', err); return null; @@ -398,7 +440,7 @@ function thriveStackPlugin(pluginConfig = {}) { }, setItem: (key, value) => { try { - localStorage.setItem(`thrivestack_${key}`, value); + setCookie(`thrivestack_${key}`, value); return true; } catch (err) { console.error('Failed to set item in storage:', err); @@ -407,7 +449,8 @@ function thriveStackPlugin(pluginConfig = {}) { }, removeItem: key => { try { - localStorage.removeItem(`thrivestack_${key}`); + 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); @@ -419,13 +462,13 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return false; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); return; } try { const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; } catch (err) { @@ -437,7 +480,7 @@ function thriveStackPlugin(pluginConfig = {}) { if (!window.ThriveStack) return null; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. User call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); return; } return { @@ -456,24 +499,18 @@ function thriveStackPlugin(pluginConfig = {}) { return Promise.reject(error); } const source = options.source || window.ThriveStack.getSource() || ''; - - // Check domain validation before processing if (!isValidDomain(source)) { const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); - console.warn(error.message); + showValidationError(error.message); if (callback && typeof callback === 'function') { callback(error); } return Promise.reject(error); } - - // Get userId without fallback const userId = options.userId || options.user_id || window.ThriveStack.userId; - - // Validation: both userId and groupId must be present if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -481,7 +518,7 @@ function thriveStackPlugin(pluginConfig = {}) { } if (!groupId) { const error = new Error('Group identify requires groupId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:'); if (callback && typeof callback === 'function') { callback(error); } @@ -507,7 +544,7 @@ function thriveStackPlugin(pluginConfig = {}) { } return result; }).catch(err => { - console.error('Failed to send group event:', err); + showValidationError('Failed to send group event:'); if (callback && typeof callback === 'function') { callback(err); } @@ -517,7 +554,7 @@ function thriveStackPlugin(pluginConfig = {}) { setApiConfig: (config = {}) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); return; } if (config.apiKey) { @@ -531,7 +568,7 @@ function thriveStackPlugin(pluginConfig = {}) { setConsent: (category, enabled) => { const source = options.source || window.ThriveStack.getSource() || ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } try { @@ -614,7 +651,7 @@ function thriveStackPlugin(pluginConfig = {}) { capturePageVisit: () => { const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } try { diff --git a/packages/analytics-plugin-thrivestack/src/index.js b/packages/analytics-plugin-thrivestack/src/index.js index ccbb435a..d07de4df 100644 --- a/packages/analytics-plugin-thrivestack/src/index.js +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -31,19 +31,42 @@ function thriveStackPlugin(pluginConfig = {}) { 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 = JSON.parse(localStorage.getItem('product_urls') || '[]'); + urlsToCheck = getCookie('product_urls'); } else if (source === 'marketing') { - urlsToCheck = JSON.parse(localStorage.getItem('marketing_urls') || '[]'); - } else { - urlsToCheck = JSON.parse(localStorage.getItem('website_urls') || '[]'); + urlsToCheck = getCookie('marketing_urls'); } if (!urlsToCheck.length) { - console.warn(`No validated ${source || 'website'} URLs found in localStorage.`); + showValidationError(`No validated ${source || 'website'} URLs found in cookies.`); return false; } @@ -55,113 +78,155 @@ function thriveStackPlugin(pluginConfig = {}) { } } - console.warn(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); + showValidationError(`Current host ${currentHost} does not match any validated ${source || 'website'} domains: ${urlsToCheck.join(', ')}`); return false; }; - const validateWebsite = async () => { try { - const productId = 'X6EcdUZFY'; - const environmentId = '2lwIjczu1'; - - const response = await fetch(`https://api.dev.app.thrivestack.ai/onboarding`, { + const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'x-api-key': pluginConfig.apiKey }, body: JSON.stringify({ - query: `query GetCAOnboardingStepDetails($input: GetCAOnboardingStepDetailsInput!) { - getCAOnboardingStepDetails(input: $input) { - productId - environmentId - moduleDetails { - moduleId - stepDetails { - stepId - isCompleted - value - subStepDetails { - subStepId - isCompleted - value - } + 'sub_step_id': '', + 'step_id': 'name_your_website', + 'module_id': 'marketing_attribution' + }) + }); + + const productResponse = await fetch('https://api.dev.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); + 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)); } } } - }`, - variables: { - input: { - productId: productId, - environmentId: environmentId + } 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); } - }) - }); - - const result = await response.json(); - console.log("API Response:", result); - - if (result.data && result.data.getCAOnboardingStepDetails) { - const moduleDetails = result.data.getCAOnboardingStepDetails.moduleDetails; - let marketingUrls = []; - let productUrls = []; - - for (const module of moduleDetails) { - - if (module.moduleId === 'marketing_attribution') { - for (const step of module.stepDetails) { - if (step.stepId === 'name_your_website' && step.value) { - try { - const stepValue = JSON.parse(step.value); - if (stepValue.website_urls && Array.isArray(stepValue.website_urls)) { - marketingUrls = stepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse marketing URLs JSON:', 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)); } } } - - if (module.moduleId === 'product_analytics') { - for (const step of module.stepDetails) { - for (const subStep of step.subStepDetails || []) { - if (subStep.subStepId === 'product_url' && subStep.value) { - try { - const subStepValue = JSON.parse(subStep.value); - if (subStepValue.website_urls && Array.isArray(subStepValue.website_urls)) { - productUrls = subStepValue.website_urls.map(url => normalizeUrl(url)); - } - } catch (parseError) { - console.error('Failed to parse product URLs JSON:', parseError); - } - } - } + } 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); } } - - - localStorage.setItem('website_urls', JSON.stringify([...productUrls, ...marketingUrls])); - localStorage.setItem('product_urls', JSON.stringify(productUrls)); - localStorage.setItem('marketing_urls', JSON.stringify(marketingUrls)); - - console.log('Stored product URLs:', productUrls); - console.log('Stored marketing URLs:', marketingUrls); - - return true; } + + 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) { - console.warn('No valid website URLs found in API response'); - return false; + 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) => { @@ -260,7 +325,7 @@ function thriveStackPlugin(pluginConfig = {}) { const isValid = await validateWebsite(); if (!isValid) { - console.warn('Website domain validation failed. Analytics calls will be blocked.'); + showValidationError('Website domain validation failed. Analytics calls will be blocked.'); } initComplete(analyticsInstance); @@ -284,15 +349,13 @@ function thriveStackPlugin(pluginConfig = {}) { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } try { - // Get deviceId without fallback const deviceId = window.ThriveStack.getDeviceId(); - // Validation: deviceId must be present if (options.source === 'marketing' && !deviceId) { const error = new Error('Identify call requires deviceId to be present'); console.error('Failed to send identify event:', error); @@ -303,7 +366,7 @@ function thriveStackPlugin(pluginConfig = {}) { } const identifyPayload = [{ - user_id: userId || '', // Use empty string only when sending the payload + user_id: userId || '', traits: traits, timestamp: options.timestamp || new Date().toISOString(), context: { @@ -340,7 +403,7 @@ function thriveStackPlugin(pluginConfig = {}) { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Identify call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. Identify call blocked.`); return; } @@ -395,21 +458,17 @@ function thriveStackPlugin(pluginConfig = {}) { const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. Page call blocked.`); + 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); @@ -420,10 +479,11 @@ function thriveStackPlugin(pluginConfig = {}) { 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); @@ -436,15 +496,13 @@ function thriveStackPlugin(pluginConfig = {}) { }, ready: (payload) => { - return initCompleted; }, - storage: { getItem: (key) => { try { - return localStorage.getItem(`thrivestack_${key}`); + return getCookie(`thrivestack_${key}`); } catch (err) { console.error('Failed to get item from storage:', err); return null; @@ -453,7 +511,7 @@ function thriveStackPlugin(pluginConfig = {}) { setItem: (key, value) => { try { - localStorage.setItem(`thrivestack_${key}`, value); + setCookie(`thrivestack_${key}`, value); return true; } catch (err) { console.error('Failed to set item in storage:', err); @@ -463,7 +521,8 @@ function thriveStackPlugin(pluginConfig = {}) { removeItem: (key) => { try { - localStorage.removeItem(`thrivestack_${key}`); + 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); @@ -472,23 +531,20 @@ function thriveStackPlugin(pluginConfig = {}) { } }, - setAnonymousId: (anonymousId) => { if (!window.ThriveStack) return false; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setAnonymousId call blocked.`); return; } try { - const currentDeviceId = window.ThriveStack.getDeviceId(); if (currentDeviceId !== anonymousId) { - - console.warn('ThriveStack device ID is automatically generated and cannot be overridden'); + showValidationError('ThriveStack device ID is automatically generated and cannot be overridden'); } return true; } catch (err) { @@ -497,14 +553,13 @@ function thriveStackPlugin(pluginConfig = {}) { } }, - user: () => { if (!window.ThriveStack) return null; const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. User call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. User call blocked.`); return; } @@ -515,9 +570,7 @@ function thriveStackPlugin(pluginConfig = {}) { }; }, - methods: { - group: (groupId, traits = {}, options = {}, callback) => { if (!window.ThriveStack) { const error = new Error('ThriveStack not initialized'); @@ -529,23 +582,20 @@ function thriveStackPlugin(pluginConfig = {}) { const source = options.source || window.ThriveStack.getSource() || ''; - // Check domain validation before processing if (!isValidDomain(source)) { const error = new Error(`Website domain not validated for source '${source}'. Group call blocked.`); - console.warn(error.message); + showValidationError(error.message); if (callback && typeof callback === 'function') { callback(error); } return Promise.reject(error); } - // Get userId without fallback const userId = options.userId || options.user_id || window.ThriveStack.userId; - // Validation: both userId and groupId must be present if (options.source === 'marketing' && !userId) { const error = new Error('Group identify requires userId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:', error); if (callback && typeof callback === 'function') { callback(error); } @@ -554,7 +604,7 @@ function thriveStackPlugin(pluginConfig = {}) { if (!groupId) { const error = new Error('Group identify requires groupId to be present'); - console.error('Group identify failed:', error); + showValidationError('Group identify failed:', error); if (callback && typeof callback === 'function') { callback(error); } @@ -585,7 +635,7 @@ function thriveStackPlugin(pluginConfig = {}) { return result; }) .catch(err => { - console.error('Failed to send group event:', err); + showValidationError('Failed to send group event:', err); if (callback && typeof callback === 'function') { callback(err); } @@ -594,11 +644,10 @@ function thriveStackPlugin(pluginConfig = {}) { }, setApiConfig: (config = {}) => { - const source = options.source || window.ThriveStack.getSource() || ''; if (!isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setApiConfig call blocked.`); return; } @@ -617,7 +666,7 @@ function thriveStackPlugin(pluginConfig = {}) { const source = options.source || window.ThriveStack.getSource() || ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. setConsent call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. setConsent call blocked.`); return false; } @@ -716,7 +765,7 @@ function thriveStackPlugin(pluginConfig = {}) { capturePageVisit: () => { const source = window.ThriveStack ? window.ThriveStack.getSource() || '' : ''; if (!window.ThriveStack || !isValidDomain(source)) { - console.warn(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); + showValidationError(`Website domain not validated for source '${source}'. capturePageVisit call blocked.`); return false; } From 082813131cc7ab1af45df2a3b5a9011c12d1e661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Fri, 9 May 2025 16:24:31 +0530 Subject: [PATCH 5/8] update: Readme file for TS analytics --- packages/analytics-plugin-thrivestack/README.md | 17 ++++++++++++++--- .../lib/index.browser.es.js | 2 +- .../lib/index.browser.js | 2 +- .../lib/index.es.js | 2 +- .../analytics-plugin-thrivestack/lib/index.js | 2 +- .../analytics-plugin-thrivestack/src/index.js | 2 +- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/README.md b/packages/analytics-plugin-thrivestack/README.md index f98aa01d..098db963 100644 --- a/packages/analytics-plugin-thrivestack/README.md +++ b/packages/analytics-plugin-thrivestack/README.md @@ -15,12 +15,15 @@ import Analytics from 'analytics' import thriveStackPlugin from '@analytics/thrivestack' const analytics = Analytics({ - app: 'awesome-app', + app: 'TS-Analytic-app', plugins: [ thriveStackPlugin({ - apiKey: 'your-thrivestack-api-key', + apiKey: 'your-thrivestack-api-key', // ⚠️ REQUIRED + source: 'website', // ⚠️ REQUIRED + options: { - // Additional configuration + // Additional configuration (optional) + debug: true, respectDoNotTrack: true, trackClicks: true, trackForms: true @@ -46,6 +49,14 @@ analytics.identify('user-123', { // 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): Identifies the source of the tracking data (e.g., 'website', 'marketing', 'product'). ``` ## Configuration Options diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js index f4769a03..4c1e33cc 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -96,7 +96,7 @@ function thriveStackPlugin(pluginConfig = {}) { }); const marketingResult = await marketingResponse.json(); const productResult = await productResponse.json(); - console.log("Marketing API Response:", marketingResult); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); console.log("Product API Response:", productResult); let marketingUrls = []; let productUrls = []; diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.js b/packages/analytics-plugin-thrivestack/lib/index.browser.js index 00e4abfa..ce6e1e3d 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -98,7 +98,7 @@ function thriveStackPlugin(pluginConfig = {}) { }); const marketingResult = await marketingResponse.json(); const productResult = await productResponse.json(); - console.log("Marketing API Response:", marketingResult); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); console.log("Product API Response:", productResult); let marketingUrls = []; let productUrls = []; diff --git a/packages/analytics-plugin-thrivestack/lib/index.es.js b/packages/analytics-plugin-thrivestack/lib/index.es.js index f4769a03..4c1e33cc 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -96,7 +96,7 @@ function thriveStackPlugin(pluginConfig = {}) { }); const marketingResult = await marketingResponse.json(); const productResult = await productResponse.json(); - console.log("Marketing API Response:", marketingResult); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); console.log("Product API Response:", productResult); let marketingUrls = []; let productUrls = []; diff --git a/packages/analytics-plugin-thrivestack/lib/index.js b/packages/analytics-plugin-thrivestack/lib/index.js index 00e4abfa..ce6e1e3d 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.js +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -98,7 +98,7 @@ function thriveStackPlugin(pluginConfig = {}) { }); const marketingResult = await marketingResponse.json(); const productResult = await productResponse.json(); - console.log("Marketing API Response:", marketingResult); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); console.log("Product API Response:", productResult); let marketingUrls = []; let productUrls = []; diff --git a/packages/analytics-plugin-thrivestack/src/index.js b/packages/analytics-plugin-thrivestack/src/index.js index d07de4df..53d3a8da 100644 --- a/packages/analytics-plugin-thrivestack/src/index.js +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -113,7 +113,7 @@ function thriveStackPlugin(pluginConfig = {}) { const marketingResult = await marketingResponse.json(); const productResult = await productResponse.json(); - console.log("Marketing API Response:", marketingResult); + console.log("Marketing API Response:", marketingResult, pluginConfig.source); console.log("Product API Response:", productResult); let marketingUrls = []; From 2cf82dfaabc3b01f3864b36c20789a2d9c46b8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Fri, 9 May 2025 19:01:54 +0530 Subject: [PATCH 6/8] update: readme file --- .../analytics-plugin-thrivestack/README.md | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/README.md b/packages/analytics-plugin-thrivestack/README.md index 098db963..e426ce20 100644 --- a/packages/analytics-plugin-thrivestack/README.md +++ b/packages/analytics-plugin-thrivestack/README.md @@ -51,30 +51,36 @@ analytics.identify('user-123', { analytics.reset() -### Basic Configuration +Basic Configuration +When initializing the ThriveStack plugin, two parameters are critical for proper functionality: -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! -- **`apiKey`** (REQUIRED): Your unique ThriveStack API key. Without this, all tracking calls will fail. -- **`source`** (REQUIRED): Identifies the source of the tracking data (e.g., 'website', 'marketing', 'product'). -``` ## Configuration Options The ThriveStack plugin accepts the following configuration options: -| Option | Description | Required | Default Value | -|:------|:------------|:---------|:--------------| -| `apiKey` | Your ThriveStack API key | Yes | - | -| `respectDoNotTrack` | Whether to respect DNT browser setting | No | `true` | -| `trackClicks` | Automatically track click events | No | `false` | -| `trackForms` | Automatically track form submissions | No | `false` | -| `enableConsent` | Enable consent management | No | `false` | -| `defaultConsent` | Default consent value | No | `false` | -| `source` | Source identifier | No | `''` | -| `batchSize` | Number of events to batch together | No | `10` | -| `batchInterval` | Interval in ms for processing event queue | No | `2000` | -| `options` | Additional options object | No | `{}` | +| 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 From fd8c1aeba28a3ce6a5c802a2144c97f8dfdc5616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Sat, 10 May 2025 13:44:58 +0530 Subject: [PATCH 7/8] update: API urls --- packages/analytics-plugin-thrivestack/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/src/index.js b/packages/analytics-plugin-thrivestack/src/index.js index 53d3a8da..c4302d3e 100644 --- a/packages/analytics-plugin-thrivestack/src/index.js +++ b/packages/analytics-plugin-thrivestack/src/index.js @@ -84,7 +84,7 @@ function thriveStackPlugin(pluginConfig = {}) { const validateWebsite = async () => { try { - const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -97,7 +97,7 @@ function thriveStackPlugin(pluginConfig = {}) { }) }); - const productResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', From 60be25d19228f32caf5e09d0375f1be07b6e4b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAniket-Thrive=E2=80=9D?= <“aniketd@thrivestack.ai”> Date: Mon, 12 May 2025 11:37:06 +0530 Subject: [PATCH 8/8] Update: API urls --- packages/analytics-plugin-thrivestack/lib/index.browser.es.js | 4 ++-- packages/analytics-plugin-thrivestack/lib/index.browser.js | 4 ++-- packages/analytics-plugin-thrivestack/lib/index.es.js | 4 ++-- packages/analytics-plugin-thrivestack/lib/index.js | 4 ++-- packages/analytics-plugin-thrivestack/src/node.js | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js index 4c1e33cc..0d5b7243 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.es.js @@ -70,7 +70,7 @@ function thriveStackPlugin(pluginConfig = {}) { }; const validateWebsite = async () => { try { - const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -82,7 +82,7 @@ function thriveStackPlugin(pluginConfig = {}) { 'module_id': 'marketing_attribution' }) }); - const productResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/analytics-plugin-thrivestack/lib/index.browser.js b/packages/analytics-plugin-thrivestack/lib/index.browser.js index ce6e1e3d..7068c840 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.browser.js +++ b/packages/analytics-plugin-thrivestack/lib/index.browser.js @@ -72,7 +72,7 @@ function thriveStackPlugin(pluginConfig = {}) { }; const validateWebsite = async () => { try { - const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -84,7 +84,7 @@ function thriveStackPlugin(pluginConfig = {}) { 'module_id': 'marketing_attribution' }) }); - const productResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/analytics-plugin-thrivestack/lib/index.es.js b/packages/analytics-plugin-thrivestack/lib/index.es.js index 4c1e33cc..0d5b7243 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.es.js +++ b/packages/analytics-plugin-thrivestack/lib/index.es.js @@ -70,7 +70,7 @@ function thriveStackPlugin(pluginConfig = {}) { }; const validateWebsite = async () => { try { - const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -82,7 +82,7 @@ function thriveStackPlugin(pluginConfig = {}) { 'module_id': 'marketing_attribution' }) }); - const productResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/analytics-plugin-thrivestack/lib/index.js b/packages/analytics-plugin-thrivestack/lib/index.js index ce6e1e3d..7068c840 100644 --- a/packages/analytics-plugin-thrivestack/lib/index.js +++ b/packages/analytics-plugin-thrivestack/lib/index.js @@ -72,7 +72,7 @@ function thriveStackPlugin(pluginConfig = {}) { }; const validateWebsite = async () => { try { - const marketingResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const marketingResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -84,7 +84,7 @@ function thriveStackPlugin(pluginConfig = {}) { 'module_id': 'marketing_attribution' }) }); - const productResponse = await fetch('https://api.dev.app.thrivestack.ai/api/caOnboardingDetails', { + const productResponse = await fetch('https://api.app.thrivestack.ai/api/caOnboardingDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/analytics-plugin-thrivestack/src/node.js b/packages/analytics-plugin-thrivestack/src/node.js index 16da9f7e..8ed59ae0 100644 --- a/packages/analytics-plugin-thrivestack/src/node.js +++ b/packages/analytics-plugin-thrivestack/src/node.js @@ -13,7 +13,7 @@ function thriveStackPlugin(pluginConfig = {}) { let instance = null; let initCompleted = false; - const API_URL = pluginConfig.apiEndpoint || 'https://api.dev.app.thrivestack.ai/api'; + const API_URL = pluginConfig.apiEndpoint || 'https://api.app.thrivestack.ai/api'; let API_KEY = pluginConfig.apiKey; const options = { respectDoNotTrack: pluginConfig.respectDoNotTrack !== false,