@fullstory/browser 2.0.7 → 2.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -11,17 +11,22 @@ import { FSApi } from '@fullstory/snippet';
11
11
  * - recordCrossDomainIFrames: FullStory can record cross-domain iFrames. Defaults to `false`. Certain limitations apply and can be found [here](https://help.fullstory.com/hc/en-us/articles/360020622514-Can-FullStory-capture-content-that-is-presented-in-iframes-#h_01F1G333PYKPGZ4B42WDBV3YKV).
12
12
  * - recordOnlyThisIFrame: FullStory can record the iFrame as its own unique session. Defaults to `false`. Additional conditions apply and can be found [here](https://help.fullstory.com/hc/en-us/articles/360020622514-Can-FullStory-capture-content-that-is-presented-in-iframes-#h_01F1G33B40Q2TPQA8MA7SF8Y5P).
13
13
  * - devMode: In dev mode FullStory won't record sessions. Any calls to SDK methods will `console.warn` that FullStory is in `devMode`. Defaults to `false`.
14
+ * - sessionUid (beta): Sets the session UID passed to the `FS('init', ...)` v2 operation.
14
15
  */
15
16
  export interface SnippetOptions {
16
17
  orgId: string;
17
- namespace?: string;
18
+ appHost?: string;
19
+ assetMapId?: string;
20
+ cookieDomain?: string;
18
21
  debug?: boolean;
22
+ devMode?: boolean;
19
23
  host?: string;
20
- script?: string;
21
- cookieDomain?: string;
24
+ namespace?: string;
22
25
  recordCrossDomainIFrames?: boolean;
23
26
  recordOnlyThisIFrame?: boolean;
24
- devMode?: boolean;
27
+ sessionUid?: string;
28
+ script?: string;
29
+ startCaptureManually?: boolean;
25
30
  }
26
31
  /**
27
32
  * A callback that will be invoked when FullStory has begun a session.
@@ -33,21 +38,7 @@ type ReadyCallback = (data: {
33
38
  sessionUrl: string;
34
39
  settings: Readonly<object>;
35
40
  }) => void;
36
- declare global {
37
- interface Window {
38
- _fs_cookie_domain?: string;
39
- _fs_debug?: boolean;
40
- _fs_dev_mode?: boolean;
41
- _fs_host?: string;
42
- _fs_initialized?: boolean;
43
- _fs_is_outer_script?: boolean;
44
- _fs_namespace?: string;
45
- _fs_org?: string;
46
- _fs_run_in_iframe?: boolean;
47
- _fs_script?: string;
48
- }
49
- }
50
- export declare const init: (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => void;
51
- export declare const isInitialized: () => boolean;
41
+ declare const init: (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => void;
42
+ declare const isInitialized: () => boolean;
52
43
  declare const FullStory: FSApi;
53
- export default FullStory;
44
+ export { FullStory, init, isInitialized };
package/dist/index.esm.js CHANGED
@@ -14,6 +14,8 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
14
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
15
  PERFORMANCE OF THIS SOFTWARE.
16
16
  ***************************************************************************** */
17
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
18
+
17
19
 
18
20
  var __assign = function() {
19
21
  __assign = Object.assign || function __assign(t) {
@@ -24,17 +26,21 @@ var __assign = function() {
24
26
  return t;
25
27
  };
26
28
  return __assign.apply(this, arguments);
29
+ };
30
+
31
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
32
+ var e = new Error(message);
33
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
27
34
  };
28
35
 
29
- var getFullStory = function () {
30
- if (window._fs_namespace) {
31
- return window[window._fs_namespace];
32
- }
33
- return undefined;
34
- };
36
+ // Module-level state set during `_init` (replaces former window `_fs_*` globals).
37
+ var _namespace;
38
+ var _initialized = false;
39
+ var _isDevMode = false;
40
+ var getFullStory = function (ns) { return (window[ns]); };
35
41
  var ensureSnippetLoaded = function () {
36
- var fs = getFullStory();
37
- if (!fs) {
42
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
43
+ if (!_namespace || !fs) {
38
44
  throw Error('FullStory is not loaded, please ensure the init function is invoked before calling FullStory API functions');
39
45
  }
40
46
  return fs;
@@ -42,21 +48,36 @@ var ensureSnippetLoaded = function () {
42
48
  var _init = function (inputOptions, readyCallback) {
43
49
  // Make a copy so we can modify `options` if desired.
44
50
  var options = __assign({}, inputOptions);
45
- if (getFullStory()) {
51
+ var ns = options.namespace || 'FS';
52
+ _namespace = ns;
53
+ if (getFullStory(ns)) {
46
54
  console.warn('The FullStory snippet has already been defined elsewhere (likely in the <head> element)');
47
55
  return;
48
56
  }
57
+ var fsInitEnv = {};
49
58
  // see README for details on the recordCrossDomainIFrames option
50
59
  if (options.recordCrossDomainIFrames) {
51
- window._fs_run_in_iframe = true;
60
+ fsInitEnv.runInIframe = true;
61
+ }
62
+ if (options.appHost) {
63
+ fsInitEnv.appHost = options.appHost;
64
+ }
65
+ if (options.assetMapId) {
66
+ fsInitEnv.assetMapId = options.assetMapId;
67
+ }
68
+ if (options.startCaptureManually) {
69
+ fsInitEnv.captureOnStartup = false;
52
70
  }
53
71
  // record the contents of this iFrame when embedded in a parent site
54
72
  if (options.recordOnlyThisIFrame) {
55
- window._fs_is_outer_script = true;
73
+ fsInitEnv.isOuterScript = true;
56
74
  }
57
75
  // Set cookie domain if it was specified.
58
76
  if (options.cookieDomain) {
59
- window._fs_cookie_domain = options.cookieDomain;
77
+ fsInitEnv.cookieDomain = options.cookieDomain;
78
+ }
79
+ if (options.sessionUid) {
80
+ fsInitEnv.sessionUid = options.sessionUid;
60
81
  }
61
82
  if (options.debug === true) {
62
83
  if (!options.script) {
@@ -67,11 +88,12 @@ var _init = function (inputOptions, readyCallback) {
67
88
  }
68
89
  }
69
90
  initFS(options);
70
- var fs = getFullStory();
91
+ var fs = getFullStory(_namespace);
71
92
  if (!fs) {
72
- console.warn('Trouble with fs');
93
+ console.warn('Failed to initialize FS snippet');
73
94
  return;
74
95
  }
96
+ fs('init', { env: fsInitEnv });
75
97
  if (readyCallback) {
76
98
  fs('observe', { type: 'start', callback: readyCallback });
77
99
  }
@@ -80,26 +102,25 @@ var _init = function (inputOptions, readyCallback) {
80
102
  fs('trackEvent', {
81
103
  name: 'FullStory Dev Mode',
82
104
  properties: {
83
- message_str: message,
105
+ message: message,
84
106
  }
85
107
  });
86
108
  fs('shutdown');
87
- window._fs_dev_mode = true;
109
+ _isDevMode = true;
88
110
  console.warn(message);
89
111
  }
90
112
  };
91
113
  var initOnce = function (message) { return function (inputOptions, readyCallback) {
92
- if (window._fs_initialized) {
114
+ if (_initialized) {
93
115
  if (message)
94
116
  console.warn(message);
95
117
  return;
96
118
  }
97
119
  _init(inputOptions, readyCallback);
98
- window._fs_initialized = true;
120
+ _initialized = true;
99
121
  }; };
100
122
  var init = initOnce('FullStory init has already been called once, additional invocations are ignored');
101
- // normalize undefined into boolean
102
- var isInitialized = function () { return !!window._fs_initialized; };
123
+ var isInitialized = function () { return _initialized; };
103
124
  var hasFullStoryWithFunction = function () {
104
125
  var testNames = [];
105
126
  for (var _i = 0; _i < arguments.length; _i++) {
@@ -113,39 +134,42 @@ var guard = function (name) { return function () {
113
134
  for (var _i = 0; _i < arguments.length; _i++) {
114
135
  args[_i] = arguments[_i];
115
136
  }
116
- if (window._fs_dev_mode) {
117
- var message = "FullStory is in dev mode and is not recording: ".concat(name, " method not executed");
137
+ if (_isDevMode) {
138
+ var message = "FullStory is in dev mode and is not capturing: ".concat(name, " method not executed");
118
139
  console.warn(message);
119
140
  return message;
120
141
  }
121
- var fs = getFullStory();
142
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
122
143
  if (hasFullStoryWithFunction(name) && fs) {
123
144
  return fs[name].apply(fs, args);
124
145
  }
125
146
  console.warn("FS.".concat(name, " not ready"));
126
147
  return null;
127
148
  }; };
128
- var FullStory = Object.assign(function (operation, options, source) {
129
- var fs = ensureSnippetLoaded();
130
- if (window._fs_dev_mode) {
131
- var message = 'FullStory is in dev mode and is not recording: method not executed';
132
- console.warn(message);
133
- return undefined;
134
- }
135
- return fs(operation, options, source);
136
- }, {
137
- anonymize: guard('anonymize'),
138
- consent: guard('consent'),
139
- disableConsole: guard('disableConsole'),
140
- enableConsole: guard('enableConsole'),
141
- event: guard('event'),
142
- getCurrentSessionURL: guard('getCurrentSessionURL'),
143
- identify: guard('identify'),
144
- log: guard('log'),
145
- restart: guard('restart'),
146
- setUserVars: guard('setUserVars'),
147
- setVars: guard('setVars'),
148
- shutdown: guard('shutdown'),
149
- });
149
+ var buildFullStoryShim = function () {
150
+ var FS = function (operation, options, source) {
151
+ var fs = ensureSnippetLoaded();
152
+ if (_isDevMode) {
153
+ var message = "FullStory is in dev mode and is not capturing: ".concat(operation, " not executed");
154
+ console.warn(message);
155
+ return undefined;
156
+ }
157
+ return fs(operation, options, source);
158
+ };
159
+ FS.anonymize = guard('anonymize');
160
+ FS.consent = guard('consent');
161
+ FS.disableConsole = guard('disableConsole');
162
+ FS.enableConsole = guard('enableConsole');
163
+ FS.event = guard('event');
164
+ FS.getCurrentSessionURL = guard('getCurrentSessionURL');
165
+ FS.identify = guard('identify');
166
+ FS.log = guard('log');
167
+ FS.restart = guard('restart');
168
+ FS.setUserVars = guard('setUserVars');
169
+ FS.setVars = guard('setVars');
170
+ FS.shutdown = guard('shutdown');
171
+ return FS;
172
+ };
173
+ var FullStory = buildFullStoryShim();
150
174
 
151
- export { FullStory as default, init, isInitialized };
175
+ export { FullStory, init, isInitialized };
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  var snippet = require('@fullstory/snippet');
6
4
 
7
5
  /******************************************************************************
@@ -18,6 +16,8 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
18
16
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19
17
  PERFORMANCE OF THIS SOFTWARE.
20
18
  ***************************************************************************** */
19
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
20
+
21
21
 
22
22
  var __assign = function() {
23
23
  __assign = Object.assign || function __assign(t) {
@@ -28,17 +28,21 @@ var __assign = function() {
28
28
  return t;
29
29
  };
30
30
  return __assign.apply(this, arguments);
31
+ };
32
+
33
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
34
+ var e = new Error(message);
35
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
31
36
  };
32
37
 
33
- var getFullStory = function () {
34
- if (window._fs_namespace) {
35
- return window[window._fs_namespace];
36
- }
37
- return undefined;
38
- };
38
+ // Module-level state set during `_init` (replaces former window `_fs_*` globals).
39
+ var _namespace;
40
+ var _initialized = false;
41
+ var _isDevMode = false;
42
+ var getFullStory = function (ns) { return (window[ns]); };
39
43
  var ensureSnippetLoaded = function () {
40
- var fs = getFullStory();
41
- if (!fs) {
44
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
45
+ if (!_namespace || !fs) {
42
46
  throw Error('FullStory is not loaded, please ensure the init function is invoked before calling FullStory API functions');
43
47
  }
44
48
  return fs;
@@ -46,21 +50,36 @@ var ensureSnippetLoaded = function () {
46
50
  var _init = function (inputOptions, readyCallback) {
47
51
  // Make a copy so we can modify `options` if desired.
48
52
  var options = __assign({}, inputOptions);
49
- if (getFullStory()) {
53
+ var ns = options.namespace || 'FS';
54
+ _namespace = ns;
55
+ if (getFullStory(ns)) {
50
56
  console.warn('The FullStory snippet has already been defined elsewhere (likely in the <head> element)');
51
57
  return;
52
58
  }
59
+ var fsInitEnv = {};
53
60
  // see README for details on the recordCrossDomainIFrames option
54
61
  if (options.recordCrossDomainIFrames) {
55
- window._fs_run_in_iframe = true;
62
+ fsInitEnv.runInIframe = true;
63
+ }
64
+ if (options.appHost) {
65
+ fsInitEnv.appHost = options.appHost;
66
+ }
67
+ if (options.assetMapId) {
68
+ fsInitEnv.assetMapId = options.assetMapId;
69
+ }
70
+ if (options.startCaptureManually) {
71
+ fsInitEnv.captureOnStartup = false;
56
72
  }
57
73
  // record the contents of this iFrame when embedded in a parent site
58
74
  if (options.recordOnlyThisIFrame) {
59
- window._fs_is_outer_script = true;
75
+ fsInitEnv.isOuterScript = true;
60
76
  }
61
77
  // Set cookie domain if it was specified.
62
78
  if (options.cookieDomain) {
63
- window._fs_cookie_domain = options.cookieDomain;
79
+ fsInitEnv.cookieDomain = options.cookieDomain;
80
+ }
81
+ if (options.sessionUid) {
82
+ fsInitEnv.sessionUid = options.sessionUid;
64
83
  }
65
84
  if (options.debug === true) {
66
85
  if (!options.script) {
@@ -71,11 +90,12 @@ var _init = function (inputOptions, readyCallback) {
71
90
  }
72
91
  }
73
92
  snippet.initFS(options);
74
- var fs = getFullStory();
93
+ var fs = getFullStory(_namespace);
75
94
  if (!fs) {
76
- console.warn('Trouble with fs');
95
+ console.warn('Failed to initialize FS snippet');
77
96
  return;
78
97
  }
98
+ fs('init', { env: fsInitEnv });
79
99
  if (readyCallback) {
80
100
  fs('observe', { type: 'start', callback: readyCallback });
81
101
  }
@@ -84,26 +104,25 @@ var _init = function (inputOptions, readyCallback) {
84
104
  fs('trackEvent', {
85
105
  name: 'FullStory Dev Mode',
86
106
  properties: {
87
- message_str: message,
107
+ message: message,
88
108
  }
89
109
  });
90
110
  fs('shutdown');
91
- window._fs_dev_mode = true;
111
+ _isDevMode = true;
92
112
  console.warn(message);
93
113
  }
94
114
  };
95
115
  var initOnce = function (message) { return function (inputOptions, readyCallback) {
96
- if (window._fs_initialized) {
116
+ if (_initialized) {
97
117
  if (message)
98
118
  console.warn(message);
99
119
  return;
100
120
  }
101
121
  _init(inputOptions, readyCallback);
102
- window._fs_initialized = true;
122
+ _initialized = true;
103
123
  }; };
104
124
  var init = initOnce('FullStory init has already been called once, additional invocations are ignored');
105
- // normalize undefined into boolean
106
- var isInitialized = function () { return !!window._fs_initialized; };
125
+ var isInitialized = function () { return _initialized; };
107
126
  var hasFullStoryWithFunction = function () {
108
127
  var testNames = [];
109
128
  for (var _i = 0; _i < arguments.length; _i++) {
@@ -117,41 +136,44 @@ var guard = function (name) { return function () {
117
136
  for (var _i = 0; _i < arguments.length; _i++) {
118
137
  args[_i] = arguments[_i];
119
138
  }
120
- if (window._fs_dev_mode) {
121
- var message = "FullStory is in dev mode and is not recording: ".concat(name, " method not executed");
139
+ if (_isDevMode) {
140
+ var message = "FullStory is in dev mode and is not capturing: ".concat(name, " method not executed");
122
141
  console.warn(message);
123
142
  return message;
124
143
  }
125
- var fs = getFullStory();
144
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
126
145
  if (hasFullStoryWithFunction(name) && fs) {
127
146
  return fs[name].apply(fs, args);
128
147
  }
129
148
  console.warn("FS.".concat(name, " not ready"));
130
149
  return null;
131
150
  }; };
132
- var FullStory = Object.assign(function (operation, options, source) {
133
- var fs = ensureSnippetLoaded();
134
- if (window._fs_dev_mode) {
135
- var message = 'FullStory is in dev mode and is not recording: method not executed';
136
- console.warn(message);
137
- return undefined;
138
- }
139
- return fs(operation, options, source);
140
- }, {
141
- anonymize: guard('anonymize'),
142
- consent: guard('consent'),
143
- disableConsole: guard('disableConsole'),
144
- enableConsole: guard('enableConsole'),
145
- event: guard('event'),
146
- getCurrentSessionURL: guard('getCurrentSessionURL'),
147
- identify: guard('identify'),
148
- log: guard('log'),
149
- restart: guard('restart'),
150
- setUserVars: guard('setUserVars'),
151
- setVars: guard('setVars'),
152
- shutdown: guard('shutdown'),
153
- });
151
+ var buildFullStoryShim = function () {
152
+ var FS = function (operation, options, source) {
153
+ var fs = ensureSnippetLoaded();
154
+ if (_isDevMode) {
155
+ var message = "FullStory is in dev mode and is not capturing: ".concat(operation, " not executed");
156
+ console.warn(message);
157
+ return undefined;
158
+ }
159
+ return fs(operation, options, source);
160
+ };
161
+ FS.anonymize = guard('anonymize');
162
+ FS.consent = guard('consent');
163
+ FS.disableConsole = guard('disableConsole');
164
+ FS.enableConsole = guard('enableConsole');
165
+ FS.event = guard('event');
166
+ FS.getCurrentSessionURL = guard('getCurrentSessionURL');
167
+ FS.identify = guard('identify');
168
+ FS.log = guard('log');
169
+ FS.restart = guard('restart');
170
+ FS.setUserVars = guard('setUserVars');
171
+ FS.setVars = guard('setVars');
172
+ FS.shutdown = guard('shutdown');
173
+ return FS;
174
+ };
175
+ var FullStory = buildFullStoryShim();
154
176
 
155
- exports.default = FullStory;
177
+ exports.FullStory = FullStory;
156
178
  exports.init = init;
157
179
  exports.isInitialized = isInitialized;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstory/browser",
3
- "version": "2.0.7",
3
+ "version": "2.1.0-beta.0",
4
4
  "description": "The official Fullstory browser SDK",
5
5
  "repository": "git://github.com/fullstorydev/fullstory-browser-sdk.git",
6
6
  "homepage": "https://github.com/fullstorydev/fullstory-browser-sdk",
@@ -25,30 +25,30 @@
25
25
  "sdk"
26
26
  ],
27
27
  "dependencies": {
28
- "@fullstory/snippet": "2.0.6"
28
+ "@fullstory/snippet": "2.1.0-beta.0"
29
+ },
30
+ "overrides": {
31
+ "brace-expansion": "^5.0.8"
29
32
  },
30
33
  "devDependencies": {
31
34
  "@babel/core": "^7.8.7",
32
- "@babel/eslint-parser": "^7.16.5",
33
35
  "@babel/preset-env": "^7.10.4",
34
36
  "@babel/register": "^7.8.6",
37
+ "@eslint/js": "^10.0.1",
35
38
  "@rollup/plugin-typescript": "^11.1.0",
36
39
  "@types/jest": "^29.5.0",
37
40
  "@types/node": "^18.15.11",
38
- "@typescript-eslint/eslint-plugin": "^5.58.0",
39
- "@typescript-eslint/parser": "^5.58.0",
40
- "eslint": "^8.6.0",
41
- "eslint-config-airbnb": "^19.0.4",
42
- "eslint-plugin-import": "^2.20.1",
43
- "eslint-plugin-jsx-a11y": "^6.5.1",
44
- "eslint-plugin-react": "^7.28.0",
45
- "jest": "^29.7.0",
41
+ "eslint": "^10.8.0",
42
+ "eslint-plugin-import-x": "^4.17.1",
43
+ "globals": "^17.8.0",
44
+ "jest": "^30.4.2",
46
45
  "jest-environment-jsdom": "^30.3.0",
47
- "rimraf": "^2.7.1",
46
+ "rimraf": "^6.1.3",
48
47
  "rollup": "^3.21.0",
49
- "ts-jest": "^29.1.0",
48
+ "ts-jest": "^29.4.12",
50
49
  "tslib": "^2.5.0",
51
50
  "typescript": "^4.2.4",
51
+ "typescript-eslint": "^8.65.0",
52
52
  "webpack": "^5.76.1"
53
53
  }
54
54
  }
package/src/index.test.ts CHANGED
@@ -1,27 +1,52 @@
1
- import {
2
- FullStory as FS,
3
- init,
4
- isInitialized,
5
- SnippetOptions,
6
- } from '.';
1
+ import type { SnippetOptions } from '.';
7
2
 
8
3
  const testOrg = '123';
9
4
 
5
+ type FsQueue = unknown[][];
6
+ type FsWithQueue = { q?: FsQueue };
7
+
8
+ const getFsQueue = (ns = 'FS'): FsQueue | undefined => (
9
+ (window[ns] as unknown as FsWithQueue | undefined)?.q
10
+ );
11
+
12
+ /** Browser-sdk init call (after snippet's own init); last matching entry wins. */
13
+ const findBrowserInitEnv = (ns = 'FS'): Record<string, unknown> | undefined => {
14
+ const initCalls = getFsQueue(ns)?.filter((call) => call[0] === 'init') ?? [];
15
+ // Snippet 2.1 pushes an init with orgId/host/script; browser SDK pushes a follow-up.
16
+ const browserInit = initCalls.length > 1 ? initCalls[initCalls.length - 1] : initCalls[0];
17
+ const initOptions = browserInit?.[1] as { env?: Record<string, unknown> } | undefined;
18
+ return initOptions?.env;
19
+ };
20
+
10
21
  beforeEach(() => {
11
- if (window[window._fs_namespace ?? '']) {
12
- delete window._fs_initialized;
13
- delete window._fs_dev_mode;
14
- delete window[window._fs_namespace ?? ''];
15
- delete window._fs_namespace;
16
- }
22
+ // Module-level init state lives in the SDK; re-import for a clean slate per test.
23
+ jest.resetModules();
24
+ Object.keys(window).forEach((key) => {
25
+ const value = window[key as keyof Window];
26
+ if (value && typeof value === 'function' && (value as FsWithQueue).q) {
27
+ delete window[key as keyof Window];
28
+ }
29
+ });
30
+ delete (window as { FS?: unknown }).FS;
17
31
  });
18
32
 
33
+ const loadSdk = async () => {
34
+ const mod = await import('.');
35
+ return {
36
+ FS: mod.FullStory,
37
+ init: mod.init,
38
+ isInitialized: mod.isInitialized,
39
+ };
40
+ };
41
+
19
42
  describe('init', () => {
20
- it('should throw error if not initialized with an orgId', () => {
43
+ it('should throw error if not initialized with an orgId', async () => {
44
+ const { init } = await loadSdk();
21
45
  expect(() => { init({} as SnippetOptions); }).toThrow();
22
46
  });
23
47
 
24
- it('should throw error if API called before init', () => {
48
+ it('should throw error if API called before init', async () => {
49
+ const { FS, init } = await loadSdk();
25
50
  try {
26
51
  FS('log', { msg: 'my log' });
27
52
  throw new Error('this should have thrown');
@@ -32,58 +57,65 @@ describe('init', () => {
32
57
  expect(() => { FS('log', { msg: 'my log' }); }).not.toThrow();
33
58
  });
34
59
 
35
- it('should add _fs_org value to window object', () => {
60
+ it('should load FS onto the window under the default namespace', async () => {
61
+ const { init } = await loadSdk();
36
62
  init({ orgId: testOrg });
37
- expect(window._fs_org).toBe(testOrg);
63
+ expect((window as unknown as { FS?: unknown }).FS).toBeDefined();
38
64
  });
39
65
 
40
- it('should add _fs_run_in_iframe value to window object', () => {
66
+ it('should pass runInIframe via FS init env', async () => {
67
+ const { init } = await loadSdk();
41
68
  init({
42
69
  orgId: testOrg,
43
70
  recordCrossDomainIFrames: true,
44
71
  });
45
- expect(window._fs_run_in_iframe).toBe(true);
72
+ expect(findBrowserInitEnv()?.runInIframe).toBe(true);
46
73
  });
47
74
 
48
- it('should add _fs_is_outer_script value to window object', () => {
75
+ it('should pass isOuterScript via FS init env', async () => {
76
+ const { init } = await loadSdk();
49
77
  init({
50
78
  orgId: testOrg,
51
79
  recordOnlyThisIFrame: true,
52
80
  });
53
- expect(window._fs_is_outer_script).toBe(true);
81
+ expect(findBrowserInitEnv()?.isOuterScript).toBe(true);
54
82
  });
55
83
 
56
- it('should add _fs_dev_mode value to window when initialized with devMode', () => {
84
+ it('should stop recording when initialized with devMode', async () => {
85
+ const { init } = await loadSdk();
57
86
  init({
58
87
  orgId: testOrg,
59
88
  devMode: true,
60
89
  });
61
90
 
62
- expect(window._fs_dev_mode).toBe(true);
91
+ const queue = getFsQueue();
92
+ expect(queue?.some((call) => call[0] === 'shutdown')).toBe(true);
63
93
  });
64
94
 
65
- it('should return whether initialized', () => {
66
- let isInit = isInitialized();
67
- expect(isInit).toBe(false);
95
+ it('should return whether initialized', async () => {
96
+ const { init, isInitialized } = await loadSdk();
97
+ expect(isInitialized()).toBe(false);
68
98
 
69
99
  init({
70
100
  orgId: testOrg,
71
101
  });
72
102
 
73
- isInit = isInitialized();
74
- expect(isInit).toBe(true);
103
+ expect(isInitialized()).toBe(true);
75
104
  });
76
105
 
77
- it('should load fs-debug.js when debug is set', () => {
106
+ it('should load fs-debug.js when debug is set', async () => {
107
+ const { init } = await loadSdk();
78
108
  init({
79
109
  orgId: testOrg,
80
110
  debug: true,
81
111
  });
82
112
 
83
- expect(window._fs_script).toMatch(/fs-debug.js$/);
113
+ const script = document.querySelector('script[data-fs-namespace]') as HTMLScriptElement | null;
114
+ expect(script?.src).toMatch(/fs-debug\.js/);
84
115
  });
85
116
 
86
- it('should pass sessionUid to FS init', () => {
117
+ it('should pass sessionUid to FS init', async () => {
118
+ const { init } = await loadSdk();
87
119
  const sessionUid = 'session-123';
88
120
 
89
121
  init({
@@ -91,11 +123,35 @@ describe('init', () => {
91
123
  sessionUid,
92
124
  });
93
125
 
94
- const queue = (window[window._fs_namespace ?? 'FS'] as unknown as { q?: unknown[][] }).q;
95
- const initCall = queue?.find((call) => call[0] === 'init');
96
- const initOptions = initCall?.[1] as { env?: { sessionUid?: string } } | undefined;
126
+ expect(findBrowserInitEnv()?.sessionUid).toBe(sessionUid);
127
+ });
128
+
129
+ it('should pass cookieDomain, appHost, and assetMapId via FS init env', async () => {
130
+ const { init } = await loadSdk();
131
+ init({
132
+ orgId: testOrg,
133
+ cookieDomain: 'example.com',
134
+ appHost: 'app.fullstory.com',
135
+ assetMapId: 'map-1',
136
+ startCaptureManually: true,
137
+ });
138
+
139
+ const env = findBrowserInitEnv();
140
+ expect(env?.cookieDomain).toBe('example.com');
141
+ expect(env?.appHost).toBe('app.fullstory.com');
142
+ expect(env?.assetMapId).toBe('map-1');
143
+ expect(env?.captureOnStartup).toBe(false);
144
+ });
145
+
146
+ it('should use a custom namespace on window', async () => {
147
+ const { init, FS } = await loadSdk();
148
+ init({
149
+ orgId: testOrg,
150
+ namespace: 'MyFS',
151
+ });
97
152
 
98
- expect(initOptions?.env?.sessionUid).toBe(sessionUid);
153
+ expect((window as unknown as { MyFS?: unknown }).MyFS).toBeDefined();
154
+ expect(() => { FS('log', { msg: 'ok' }); }).not.toThrow();
99
155
  });
100
156
  });
101
157
 
@@ -111,7 +167,8 @@ describe('devMode', () => {
111
167
  consoleWarnedMessage = undefined;
112
168
  });
113
169
 
114
- it('should return a message for functions invoked when in devMode', () => {
170
+ it('should return a message for functions invoked when in devMode', async () => {
171
+ const { init, FS } = await loadSdk();
115
172
  expect(consoleWarnedMessage).toBeUndefined();
116
173
  init({
117
174
  orgId: testOrg,
@@ -127,7 +184,8 @@ describe('devMode', () => {
127
184
  });
128
185
 
129
186
  describe('getCurrentSessionURL', () => {
130
- it('should return null before fs.js is fully bootstrapped', () => {
187
+ it('should return null before fs.js is fully bootstrapped', async () => {
188
+ const { init, FS } = await loadSdk();
131
189
  init({ orgId: testOrg });
132
190
  // in theory, this is a race condition - assuming that fs.js
133
191
  // can't load by the time the following statement is executed
@@ -137,7 +195,8 @@ describe('getCurrentSessionURL', () => {
137
195
  });
138
196
 
139
197
  describe('typescript safety', () => {
140
- it('provides type assistance matching the api', () => {
198
+ it('provides type assistance matching the api', async () => {
199
+ const { init, FS } = await loadSdk();
141
200
  // Just a quick non-exhaustive check that types are working as expected.
142
201
  // The "@ts-expect-error" declaration will fail if the types do NOT throw
143
202
  // an error.
@@ -190,7 +249,8 @@ describe('typescript safety', () => {
190
249
  expect(true).toBe(true);
191
250
  });
192
251
 
193
- it('allows the optional "source" param', () => {
252
+ it('allows the optional "source" param', async () => {
253
+ const { init, FS } = await loadSdk();
194
254
  init({ orgId: testOrg });
195
255
 
196
256
  FS('setProperties', {
@@ -223,6 +283,7 @@ describe('typescript safety', () => {
223
283
 
224
284
  // NOTE: don't run this test, it will hang since fs.js isn't really running. It's only for typescript safety checks.
225
285
  it.skip('provides type assistance for the async api', async () => {
286
+ const { init, FS } = await loadSdk();
226
287
  init({ orgId: testOrg });
227
288
 
228
289
  const disconnector = await FS('observeAsync', { type: 'start', callback: () => console.log('STARTED') });
package/src/index.ts CHANGED
@@ -38,34 +38,29 @@ export interface SnippetOptions {
38
38
  */
39
39
  type ReadyCallback = (data: { sessionUrl: string, settings: Readonly<object> }) => void;
40
40
 
41
- declare global {
42
- interface Window {
43
- _fs_app_host?: string;
44
- _fs_asset_map_id?: string;
45
- _fs_capture_on_startup?: boolean;
46
- _fs_cookie_domain?: string;
47
- _fs_debug?: boolean;
48
- _fs_dev_mode?: boolean;
49
- _fs_host?: string;
50
- _fs_initialized?: boolean;
51
- _fs_is_outer_script?: boolean;
52
- _fs_namespace?: string;
53
- _fs_org?: string;
54
- _fs_run_in_iframe?: boolean;
55
- _fs_script?: string;
56
- }
57
- }
58
-
59
- const getFullStory = (): FSApi | undefined => {
60
- if (window._fs_namespace) {
61
- return window[window._fs_namespace];
62
- }
63
- return undefined;
41
+ /** Env overrides passed to `FS('init', { env })` after the snippet loads. */
42
+ type FsInitEnv = {
43
+ appHost?: string;
44
+ assetMapId?: string;
45
+ captureOnStartup?: boolean;
46
+ cookieDomain?: string;
47
+ isOuterScript?: boolean;
48
+ runInIframe?: boolean;
49
+ sessionUid?: string;
64
50
  };
65
51
 
52
+ // Module-level state set during `_init` (replaces former window `_fs_*` globals).
53
+ let _namespace: string | undefined;
54
+ let _initialized = false;
55
+ let _isDevMode = false;
56
+
57
+ const getFullStory = (ns: string): FSApi | undefined => (
58
+ (window as unknown as Record<string, FSApi | undefined>)[ns]
59
+ );
60
+
66
61
  const ensureSnippetLoaded = (): FSApi => {
67
- const fs = getFullStory();
68
- if (!fs) {
62
+ const fs = _namespace ? getFullStory(_namespace) : undefined;
63
+ if (!_namespace || !fs) {
69
64
  throw Error(
70
65
  'FullStory is not loaded, please ensure the init function is invoked before calling FullStory API functions'
71
66
  );
@@ -77,36 +72,45 @@ const ensureSnippetLoaded = (): FSApi => {
77
72
  const _init = (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => {
78
73
  // Make a copy so we can modify `options` if desired.
79
74
  const options = { ...inputOptions };
80
- if (getFullStory()) {
75
+ const ns = options.namespace || 'FS';
76
+ _namespace = ns;
77
+
78
+ if (getFullStory(ns)) {
81
79
  console.warn('The FullStory snippet has already been defined elsewhere (likely in the <head> element)');
82
80
  return;
83
81
  }
84
82
 
83
+ const fsInitEnv: FsInitEnv = {};
84
+
85
85
  // see README for details on the recordCrossDomainIFrames option
86
86
  if (options.recordCrossDomainIFrames) {
87
- window._fs_run_in_iframe = true;
87
+ fsInitEnv.runInIframe = true;
88
88
  }
89
89
 
90
90
  if (options.appHost) {
91
- window._fs_app_host = options.appHost;
91
+ fsInitEnv.appHost = options.appHost;
92
92
  }
93
93
 
94
94
  if (options.assetMapId) {
95
- window._fs_asset_map_id = options.assetMapId;
95
+ fsInitEnv.assetMapId = options.assetMapId;
96
96
  }
97
97
 
98
98
  if (options.startCaptureManually) {
99
- window._fs_capture_on_startup = false;
99
+ fsInitEnv.captureOnStartup = false;
100
100
  }
101
101
 
102
102
  // record the contents of this iFrame when embedded in a parent site
103
103
  if (options.recordOnlyThisIFrame) {
104
- window._fs_is_outer_script = true;
104
+ fsInitEnv.isOuterScript = true;
105
105
  }
106
106
 
107
107
  // Set cookie domain if it was specified.
108
108
  if (options.cookieDomain) {
109
- window._fs_cookie_domain = options.cookieDomain;
109
+ fsInitEnv.cookieDomain = options.cookieDomain;
110
+ }
111
+
112
+ if (options.sessionUid) {
113
+ fsInitEnv.sessionUid = options.sessionUid;
110
114
  }
111
115
 
112
116
  if (options.debug === true) {
@@ -119,16 +123,14 @@ const _init = (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => {
119
123
 
120
124
  initFS(options);
121
125
 
122
- const fs = getFullStory();
126
+ const fs = getFullStory(_namespace);
123
127
 
124
128
  if (!fs) {
125
129
  console.warn('Failed to initialize FS snippet');
126
130
  return;
127
131
  }
128
132
 
129
- if (options.sessionUid) {
130
- fs('init', { env: { sessionUid: options.sessionUid } });
131
- }
133
+ fs('init', { env: fsInitEnv });
132
134
 
133
135
  if (readyCallback) {
134
136
  fs('observe', { type: 'start', callback: readyCallback });
@@ -143,38 +145,37 @@ const _init = (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => {
143
145
  }
144
146
  });
145
147
  fs('shutdown');
146
- window._fs_dev_mode = true;
148
+ _isDevMode = true;
147
149
  console.warn(message);
148
150
  }
149
151
  };
150
152
 
151
153
  const initOnce = (message) => (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => {
152
- if (window._fs_initialized) {
154
+ if (_initialized) {
153
155
  if (message) console.warn(message);
154
156
  return;
155
157
  }
156
158
  _init(inputOptions, readyCallback);
157
- window._fs_initialized = true;
159
+ _initialized = true;
158
160
  };
159
161
 
160
- export const init = initOnce('FullStory init has already been called once, additional invocations are ignored');
162
+ const init = initOnce('FullStory init has already been called once, additional invocations are ignored');
161
163
 
162
- // normalize undefined into boolean
163
- export const isInitialized = () => !!window._fs_initialized;
164
+ const isInitialized = () => _initialized;
164
165
 
165
- const hasFullStoryWithFunction = (...testNames) => {
166
+ const hasFullStoryWithFunction = (...testNames:string[]) => {
166
167
  const fs = ensureSnippetLoaded();
167
168
  return testNames.every((current) => fs[current]);
168
169
  };
169
170
 
170
171
  const guard = (name) => (...args) => {
171
- if (window._fs_dev_mode) {
172
+ if (_isDevMode) {
172
173
  const message = `FullStory is in dev mode and is not capturing: ${name} method not executed`;
173
174
  console.warn(message);
174
175
  return message;
175
176
  }
176
177
 
177
- const fs = getFullStory();
178
+ const fs = _namespace ? getFullStory(_namespace) : undefined;
178
179
  if (hasFullStoryWithFunction(name) && fs) {
179
180
  return fs[name](...args);
180
181
  }
@@ -186,7 +187,7 @@ const buildFullStoryShim = (): FSApi => {
186
187
  const FS = (operation, options, source) => {
187
188
  const fs = ensureSnippetLoaded();
188
189
 
189
- if (window._fs_dev_mode) {
190
+ if (_isDevMode) {
190
191
  const message = `FullStory is in dev mode and is not capturing: ${operation} not executed`;
191
192
  console.warn(message);
192
193
  return undefined;
@@ -210,4 +211,6 @@ const buildFullStoryShim = (): FSApi => {
210
211
  return FS as FSApi;
211
212
  };
212
213
 
213
- export const FullStory: FSApi = buildFullStoryShim();
214
+ const FullStory: FSApi = buildFullStoryShim();
215
+
216
+ export { FullStory, init, isInitialized };