posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
591 lines • 28.4 kB
JavaScript
"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