UNPKG

posthog-js

Version:

Posthog-js allows you to automatically capture usage and send events to PostHog.

591 lines • 28.4 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostHogSurveys = void 0; var globals_1 = require("@posthog/browser-common/utils/globals"); var promise_utils_1 = require("@posthog/browser-common/utils/promise-utils"); var core_1 = require("@posthog/core"); var constants_1 = require("./constants"); var posthog_surveys_types_1 = require("./posthog-surveys-types"); var extension_tokens_1 = require("./extension-tokens"); var survey_utils_1 = require("./utils/survey-utils"); var SURVEY_NOT_LOADED = 'SDK is not enabled or survey functionality is not yet loaded'; var SURVEY_DISABLED = 'Disabled. Not loading surveys.'; var PostHogSurveys = /** @class */ (function () { function PostHogSurveys(_configSource, _initialClientState) { var _this = this; this._configSource = _configSource; this._initialClientState = _initialClientState; this.name = extension_tokens_1.SurveysExtension; this._surveyEventReceiver = null; this._surveyManager = null; this._isInitializingSurveys = false; this._surveyCallbacks = []; // Promise for in-flight survey fetch - allows multiple callers to await the same request this._getSurveysInFlightPromise = null; // Backs off the stale-cache refresh for one TTL after a failure, so a surveys-API outage can't // turn the ~1s display poll into a per-poll request storm. this._lastSurveyRefreshFailedAt = null; this._disposed = false; this._renderTimeouts = new Set(); this.onRemoteConfig = function (result) { if (_this._disposed) { return; } // only load surveys if they are enabled and there are surveys to load if (_this._config.disableSurveys) { return; } if (!result.ok) { // Failure behaves like a response without a surveys key: not loaded. return survey_utils_1.SURVEY_LOGGER.warn('Remote config unavailable. Not loading surveys.'); } var surveys = result.config['surveys']; if ((0, core_1.isNullish)(surveys)) { return survey_utils_1.SURVEY_LOGGER.warn('Flags not loaded yet. Not loading surveys.'); } _this._isSurveysEnabled = (0, core_1.isBoolean)(surveys) ? surveys : surveys.length > 0; survey_utils_1.SURVEY_LOGGER.info("flags response received, isSurveysEnabled: ".concat(_this._isSurveysEnabled)); _this.loadIfEnabled(); }; } PostHogSurveys.prototype.setup = function (client) { var _this = this; if (this._disposed) { return; } this._initializingClient = client; return (0, promise_utils_1.continueWith)(client.kv.initialize(), function () { if (_this._initializingClient !== client || _this._disposed) { return; } _this._initializingClient = undefined; _this._client = client; var subscription = client.onRemoteConfig(_this.onRemoteConfig); if (_this._disposed) { subscription.dispose(); return; } _this._remoteConfigSubscription = subscription; _this.loadIfEnabled(); }); }; PostHogSurveys.prototype.dispose = function () { var _a, _b, _c, _d; if (this._disposed) { return; } this._disposed = true; this._initializingClient = undefined; this._client = undefined; (_a = this._remoteConfigSubscription) === null || _a === void 0 ? void 0 : _a.dispose(); this._remoteConfigSubscription = undefined; (_b = this._surveyEventReceiver) === null || _b === void 0 ? void 0 : _b.dispose(); this._surveyEventReceiver = null; (_d = (_c = this._surveyManager) === null || _c === void 0 ? void 0 : _c.dispose) === null || _d === void 0 ? void 0 : _d.call(_c); this._surveyManager = null; this._surveyCallbacks = []; this._getSurveysInFlightPromise = null; this._renderTimeouts.forEach(function (timeout) { return clearTimeout(timeout); }); this._renderTimeouts.clear(); }; Object.defineProperty(PostHogSurveys.prototype, "_config", { get: function () { return this._configSource.get(); }, enumerable: false, configurable: true }); PostHogSurveys.prototype.initialize = function () { this.loadIfEnabled(); }; PostHogSurveys.prototype.reset = function () { var _a; try { // Drop in-memory event/action activations too; they aren't in persistence (which // reset() has already cleared), so without this an armed-but-unshown survey would // survive a logout/account switch that doesn't reload the page. (_a = this._surveyEventReceiver) === null || _a === void 0 ? void 0 : _a.reset(); localStorage.removeItem('lastSeenSurveyDate'); var surveyKeys = []; for (var i = 0; i < localStorage.length; i++) { var key = localStorage.key(i); if ((key === null || key === void 0 ? void 0 : key.startsWith(survey_utils_1.SURVEY_SEEN_PREFIX)) || (key === null || key === void 0 ? void 0 : key.startsWith(survey_utils_1.SURVEY_IN_PROGRESS_PREFIX))) { surveyKeys.push(key); } } surveyKeys.forEach(function (key) { return localStorage.removeItem(key); }); } catch (_b) { // localStorage is not always available (e.g. in cross-origin iframes); resetting survey state is best-effort. } }; PostHogSurveys.prototype.loadIfEnabled = function () { var _this = this; if (this._disposed || !this._client) { return; } var config = this._config; // Initial guard clauses if (this._surveyManager) { return; } // Already loaded if (this._isInitializingSurveys) { survey_utils_1.SURVEY_LOGGER.info('Already initializing surveys, skipping...'); return; } if (config.disableSurveys) { survey_utils_1.SURVEY_LOGGER.info(SURVEY_DISABLED); return; } if (config.cookielessMode && this._configSource.isOptedOut()) { survey_utils_1.SURVEY_LOGGER.info('Not loading surveys in cookieless mode without consent.'); return; } var phExtensions = this._configSource.getExtensions(); if (!phExtensions) { survey_utils_1.SURVEY_LOGGER.error('PostHog Extensions not found.'); return; } // waiting for remote config to load // if surveys is forced enable (like external surveys), ignore the remote config and load surveys if ((0, core_1.isUndefined)(this._isSurveysEnabled) && !config.advancedEnableSurveys) { return; } var isSurveysEnabled = this._isSurveysEnabled || config.advancedEnableSurveys; this._isInitializingSurveys = true; try { var generateSurveys = phExtensions.generateSurveys; if (generateSurveys) { // Surveys code is already loaded this._completeSurveyInitialization(generateSurveys, isSurveysEnabled); this._isInitializingSurveys = false; return; } // If we reach here, surveys code is not loaded yet var loadExternalDependency = phExtensions.loadExternalDependency; if (!loadExternalDependency) { // Cannot load surveys code this._handleSurveyLoadError(constants_1.LOAD_EXT_NOT_FOUND); this._isInitializingSurveys = false; return; } // Keep the initialization guard active until the dependency callback completes. loadExternalDependency(function (err) { try { if (_this._disposed) { return; } var loadedExtensions = _this._configSource.getExtensions(); if (err || !(loadedExtensions === null || loadedExtensions === void 0 ? void 0 : loadedExtensions.generateSurveys)) { _this._handleSurveyLoadError('Could not load surveys script', err); } else { // Need to get the function reference again inside the callback _this._completeSurveyInitialization(loadedExtensions.generateSurveys, isSurveysEnabled); } } finally { _this._isInitializingSurveys = false; } }); } catch (e) { this._isInitializingSurveys = false; this._handleSurveyLoadError('Error initializing surveys', e); throw e; } }; /** Helper to finalize survey initialization */ PostHogSurveys.prototype._completeSurveyInitialization = function (generateSurveysFn, isSurveysEnabled) { if (this._disposed) { return; } this._surveyManager = generateSurveysFn(isSurveysEnabled); this._surveyEventReceiver = this._configSource.createEventReceiver(); survey_utils_1.SURVEY_LOGGER.info('Surveys loaded successfully'); this._notifySurveyCallbacks({ isLoaded: true }); }; /** Helper to handle errors during survey loading */ PostHogSurveys.prototype._handleSurveyLoadError = function (message, error) { survey_utils_1.SURVEY_LOGGER.error(message, error); this._notifySurveyCallbacks({ isLoaded: false, error: message }); }; /** * Register a callback that runs when surveys are initialized. * ### Usage: * * posthog.onSurveysLoaded((surveys) => { * // You can work with all surveys * console.log('All available surveys:', surveys) * * // Or get active matching surveys * posthog.getActiveMatchingSurveys((activeMatchingSurveys) => { * if (activeMatchingSurveys.length > 0) { * posthog.renderSurvey(activeMatchingSurveys[0].id, '#survey-container') * } * }) * }) * * @param {Function} callback The callback function will be called when surveys are loaded or updated. * It receives the array of all surveys and a context object with error status. * @returns {Function} A function that can be called to unsubscribe the listener. */ PostHogSurveys.prototype.onSurveysLoaded = function (callback) { var _this = this; this._surveyCallbacks.push(callback); if (this._surveyManager) { this._notifySurveyCallbacks({ isLoaded: true, }); } // Return unsubscribe function return function () { _this._surveyCallbacks = _this._surveyCallbacks.filter(function (cb) { return cb !== callback; }); }; }; PostHogSurveys.prototype.getSurveys = function (callback, forceReload) { var _this = this; var _a; if (forceReload === void 0) { forceReload = false; } var client = (_a = this._client) !== null && _a !== void 0 ? _a : this._initialClientState; if (!client || this._disposed) { return; } if (this._config.disableSurveys) { survey_utils_1.SURVEY_LOGGER.info(SURVEY_DISABLED); return callback([]); } var surveys = client.kv.get(constants_1.SURVEYS); if (surveys && !forceReload) { callback(surveys, { isLoaded: true }); if (this._shouldBackgroundRefreshSurveys()) { this.getSurveys(function () { }, true); } return; } if (this._getSurveysInFlightPromise) { void this._getSurveysInFlightPromise .then(function (_a) { var surveys = _a.surveys, context = _a.context; if (!_this._disposed) { callback(surveys, context); } }) .catch(function (error) { return survey_utils_1.SURVEY_LOGGER.error('Error in survey callback', error); }); return; } var request = this._sendSurveysRequest('/api/surveys/', { method: 'GET', query: { token: client.projectToken }, sentAt: 'query', timeoutMs: this._config.requestTimeoutMs, }).then(function (response) { try { return _this._handleSurveyResponse(client, response); } catch (error) { survey_utils_1.SURVEY_LOGGER.error('Error processing surveys response', error); return _this._handleSurveyResponse(client, { statusCode: 0, error: error }); } }, function (error) { return _this._handleSurveyResponse(client, { statusCode: 0, error: error }); }); this._getSurveysInFlightPromise = request; var clearInFlight = function () { if (_this._getSurveysInFlightPromise === request) { _this._getSurveysInFlightPromise = null; } }; void request .then(function (result) { clearInFlight(); if (!_this._disposed) { callback(result.surveys, result.context); } }, clearInFlight) .catch(function (error) { return survey_utils_1.SURVEY_LOGGER.error('Error in survey callback', error); }); }; PostHogSurveys.prototype._sendSurveysRequest = function (path, init) { var client = this._client; if (!client) { return new Promise(function (resolve) { return resolve({ statusCode: 0, error: new Error(SURVEY_NOT_LOADED) }); }); } return client.sendRequest(path, init); }; PostHogSurveys.prototype._handleSurveyResponse = function (client, response) { var _a; var _b; if (this._disposed) { return { surveys: [], context: { isLoaded: false, error: SURVEY_NOT_LOADED } }; } var statusCode = response.statusCode; if (statusCode !== 200 || !response.json) { var error = "Surveys API could not be loaded, status: ".concat(statusCode); if (statusCode !== 0) { survey_utils_1.SURVEY_LOGGER.error(error); } else if (!response.error) { survey_utils_1.SURVEY_LOGGER.warn(error); } this._lastSurveyRefreshFailedAt = Date.now(); return { surveys: [], context: { isLoaded: false, error: error } }; } this._lastSurveyRefreshFailedAt = null; var surveys = response.json.surveys || []; var eventOrActionBasedSurveys = surveys.filter(function (survey) { return (0, survey_utils_1.isSurveyRunning)(survey) && ((0, survey_utils_1.doesSurveyActivateByEvent)(survey) || (0, survey_utils_1.doesSurveyActivateByAction)(survey)); }); if (eventOrActionBasedSurveys.length > 0) { (_b = this._surveyEventReceiver) === null || _b === void 0 ? void 0 : _b.register(eventOrActionBasedSurveys); } // Stamp when these definitions were fetched so the split-storage loader can tell a fresher // main-blob write-back from a stale `__surveys` entry. client.kv.set((_a = {}, _a[constants_1.SURVEYS] = surveys, _a[constants_1.SURVEYS_LOADED_AT] = Date.now(), _a)); return { surveys: surveys, context: { isLoaded: true } }; }; /** * Whether to kick off a background refresh of the cached definitions: the cache is stale, no * fetch is already in flight, and we're not backing off after a recent failure. */ PostHogSurveys.prototype._shouldBackgroundRefreshSurveys = function () { return this._isSurveyCacheStale() && !this._getSurveysInFlightPromise && !this._isSurveyRefreshBackingOff(); }; /** * Whether the cached `$surveys` definitions have aged past their TTL. Returns false when no * timestamp is recorded (e.g. surveys injected directly in tests) so the cache stays valid. */ PostHogSurveys.prototype._isSurveyCacheStale = function () { var _a, _b; var surveysLoadedAt = (_b = ((_a = this._client) !== null && _a !== void 0 ? _a : this._initialClientState)) === null || _b === void 0 ? void 0 : _b.kv.get(constants_1.SURVEYS_LOADED_AT); return (0, core_1.isNumber)(surveysLoadedAt) && Date.now() - surveysLoadedAt > constants_1.SURVEYS_CACHE_TTL_MS; }; PostHogSurveys.prototype._isSurveyRefreshBackingOff = function () { return ((0, core_1.isNumber)(this._lastSurveyRefreshFailedAt) && Date.now() - this._lastSurveyRefreshFailedAt < constants_1.SURVEYS_REFRESH_BACKOFF_MS); }; /** * Marks a survey as seen for the current device, mirroring the local state the SDK records * when it shows or sends a survey itself. * * Use this when you display surveys through your own backend/integration (so the SDK never * captures the `survey shown`/`sent`/`dismissed` events) and still want PostHog's display * logic to honour the "already seen" and wait-period checks on subsequent page loads. * * Note: surveys configured to repeat (`schedule: 'always'` or event `repeatedActivation`) * intentionally bypass the seen check, so marking them as seen will not stop them showing. * * @param surveyId The ID of the survey to mark as seen. * @param options Optional settings. `iteration` is the survey's current iteration number, if any. */ PostHogSurveys.prototype.markSurveyAsSeen = function (surveyId, options) { var _a; var survey = { id: surveyId, current_iteration: (_a = options === null || options === void 0 ? void 0 : options.iteration) !== null && _a !== void 0 ? _a : null }; (0, survey_utils_1.setSurveySeenOnLocalStorage)(survey); try { localStorage.setItem('lastSeenSurveyDate', new Date().toISOString()); } catch (_b) { // localStorage is not always available (e.g. in cross-origin iframes); best-effort only. } }; /** Helper method to notify all registered callbacks */ PostHogSurveys.prototype._notifySurveyCallbacks = function (context) { var e_1, _a; try { for (var _b = __values(this._surveyCallbacks), _c = _b.next(); !_c.done; _c = _b.next()) { var callback = _c.value; try { if (!context.isLoaded) { return callback([], context); } this.getSurveys(callback); } catch (error) { survey_utils_1.SURVEY_LOGGER.error('Error in survey callback', error); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } }; PostHogSurveys.prototype.getActiveMatchingSurveys = function (callback, forceReload) { if (forceReload === void 0) { forceReload = false; } if ((0, core_1.isNullish)(this._surveyManager)) { survey_utils_1.SURVEY_LOGGER.warn('init was not called'); return; } return this._surveyManager.getActiveMatchingSurveys(callback, forceReload); }; PostHogSurveys.prototype._getSurveyById = function (surveyId) { var survey = null; this.getSurveys(function (surveys) { var _a; survey = (_a = surveys.find(function (x) { return x.id === surveyId; })) !== null && _a !== void 0 ? _a : null; }); return survey; }; PostHogSurveys.prototype._checkSurveyEligibility = function (surveyId) { if ((0, core_1.isNullish)(this._surveyManager)) { return { eligible: false, reason: SURVEY_NOT_LOADED }; } var survey = typeof surveyId === 'string' ? this._getSurveyById(surveyId) : surveyId; if (!survey) { return { eligible: false, reason: 'Survey not found' }; } return this._surveyManager.checkSurveyEligibility(survey); }; PostHogSurveys.prototype._checkSurveyRenderability = function (surveyId) { if ((0, core_1.isNullish)(this._surveyManager)) { return { eligible: false, reason: SURVEY_NOT_LOADED }; } var survey = typeof surveyId === 'string' ? this._getSurveyById(surveyId) : surveyId; if (!survey) { return { eligible: false, reason: 'Survey not found' }; } return this._surveyManager.checkSurveyRenderability(survey); }; PostHogSurveys.prototype.canRenderSurvey = function (surveyId) { if ((0, core_1.isNullish)(this._surveyManager)) { survey_utils_1.SURVEY_LOGGER.warn('init was not called'); return { visible: false, disabledReason: SURVEY_NOT_LOADED }; } var eligibility = this._checkSurveyRenderability(surveyId); return { visible: eligibility.eligible, disabledReason: eligibility.reason }; }; PostHogSurveys.prototype.canRenderSurveyAsync = function (surveyId, forceReload) { var _this = this; // Ensure surveys are loaded before checking // Using Promise to wrap the callback-based getSurveys method if ((0, core_1.isNullish)(this._surveyManager)) { survey_utils_1.SURVEY_LOGGER.warn('init was not called'); return Promise.resolve({ visible: false, disabledReason: SURVEY_NOT_LOADED, }); } // eslint-disable-next-line compat/compat return new Promise(function (resolve) { _this.getSurveys(function (surveys) { var _a; var survey = (_a = surveys.find(function (x) { return x.id === surveyId; })) !== null && _a !== void 0 ? _a : null; if (!survey) { resolve({ visible: false, disabledReason: 'Survey not found' }); } else { var eligibility = _this._checkSurveyRenderability(survey); resolve({ visible: eligibility.eligible, disabledReason: eligibility.reason }); } }, forceReload); }); }; PostHogSurveys.prototype.renderSurvey = function (surveyId, selector, properties) { var _this = this; var _a; if ((0, core_1.isNullish)(this._surveyManager)) { survey_utils_1.SURVEY_LOGGER.warn('init was not called'); return; } var survey = typeof surveyId === 'string' ? this._getSurveyById(surveyId) : surveyId; if (!(survey === null || survey === void 0 ? void 0 : survey.id)) { survey_utils_1.SURVEY_LOGGER.warn('Survey not found'); return; } if (!survey_utils_1.IN_APP_SURVEY_TYPES.includes(survey.type)) { survey_utils_1.SURVEY_LOGGER.warn("Surveys of type ".concat(survey.type, " cannot be rendered in the app")); return; } var elem = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.querySelector(selector); if (!elem) { survey_utils_1.SURVEY_LOGGER.warn('Survey element not found'); return; } if ((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.surveyPopupDelaySeconds) { survey_utils_1.SURVEY_LOGGER.info("Rendering survey ".concat(survey.id, " with delay of ").concat(survey.appearance.surveyPopupDelaySeconds, " seconds")); var timeout_1 = setTimeout(function () { var _a, _b; _this._renderTimeouts.delete(timeout_1); if (_this._disposed) { return; } survey_utils_1.SURVEY_LOGGER.info("Rendering survey ".concat(survey.id, " with delay of ").concat((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.surveyPopupDelaySeconds, " seconds")); (_b = _this._surveyManager) === null || _b === void 0 ? void 0 : _b.renderSurvey(survey, elem, properties); survey_utils_1.SURVEY_LOGGER.info("Survey ".concat(survey.id, " rendered")); }, survey.appearance.surveyPopupDelaySeconds * 1000); this._renderTimeouts.add(timeout_1); return; } this._surveyManager.renderSurvey(survey, elem, properties); }; PostHogSurveys.prototype.displaySurvey = function (surveyId, options) { var _a; if ((0, core_1.isNullish)(this._surveyManager)) { survey_utils_1.SURVEY_LOGGER.warn('init was not called'); return; } var survey = this._getSurveyById(surveyId); if (!survey) { survey_utils_1.SURVEY_LOGGER.warn('Survey not found'); return; } var surveyToDisplay = survey; if (((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.surveyPopupDelaySeconds) && options.ignoreDelay) { surveyToDisplay = __assign(__assign({}, survey), { appearance: __assign(__assign({}, survey.appearance), { surveyPopupDelaySeconds: 0 }) }); } if (options.displayType !== posthog_surveys_types_1.DisplaySurveyType.Popover && options.initialResponses) { survey_utils_1.SURVEY_LOGGER.warn('initialResponses is only supported for popover surveys. prefill will not be applied.'); } if (options.ignoreConditions === false) { // Explicit display goes through eligibility only, not renderability: the event/action // trigger state lives in memory and is irrelevant when the caller asks to show the survey. var eligibility = this._checkSurveyEligibility(survey); if (!eligibility.eligible) { survey_utils_1.SURVEY_LOGGER.warn('Survey is not eligible to be displayed: ', eligibility.reason); return; } } if (options.displayType === posthog_surveys_types_1.DisplaySurveyType.Inline) { this.renderSurvey(surveyToDisplay, options.selector, options.properties); return; } this._surveyManager.handlePopoverSurvey(surveyToDisplay, options); }; PostHogSurveys.prototype.cancelPendingSurvey = function (surveyId) { if ((0, core_1.isNullish)(this._surveyManager)) { survey_utils_1.SURVEY_LOGGER.warn('init was not called'); return; } this._surveyManager.cancelSurvey(surveyId); }; PostHogSurveys.prototype.handlePageUnload = function () { var _a, _b; (_b = (_a = this._surveyManager) === null || _a === void 0 ? void 0 : _a.handlePageUnload) === null || _b === void 0 ? void 0 : _b.call(_a); }; return PostHogSurveys; }()); exports.PostHogSurveys = PostHogSurveys; //# sourceMappingURL=posthog-surveys.js.map