@fullstory/browser 2.0.8 → 2.1.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/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  Fullstory's browser SDK lets you manage Fullstory data capture on your site as well as retrieve deep links to session replays and send your own custom events. More information about the Fullstory API can be found at https://developer.fullstory.com.
6
6
 
7
+ Version 2.1.0 updates the sdk to use v2.1 of the Fullstory snippet. No external updates are required. Please reference our developer docs for a list of [what's new in Snippet 2.1](https://developer.fullstory.com/browser/getting-started/#whats-new-in-version-21).
8
+
7
9
  > **NOTE:** this is the documentation for version 2. For version 1 documentation, please see [@fullstory/browser@1.7.1](https://www.npmjs.com/package/@fullstory/browser/v/1.7.1).
8
10
 
9
11
  ## Install the SDK
package/dist/index.d.ts CHANGED
@@ -38,23 +38,6 @@ type ReadyCallback = (data: {
38
38
  sessionUrl: string;
39
39
  settings: Readonly<object>;
40
40
  }) => void;
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
41
  declare const init: (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => void;
59
42
  declare const isInitialized: () => boolean;
60
43
  declare const FullStory: FSApi;
package/dist/index.esm.js CHANGED
@@ -14,7 +14,7 @@ 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 */
17
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
18
18
 
19
19
 
20
20
  var __assign = function() {
@@ -26,17 +26,21 @@ var __assign = function() {
26
26
  return t;
27
27
  };
28
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;
29
34
  };
30
35
 
31
- var getFullStory = function () {
32
- if (window._fs_namespace) {
33
- return window[window._fs_namespace];
34
- }
35
- return undefined;
36
- };
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]); };
37
41
  var ensureSnippetLoaded = function () {
38
- var fs = getFullStory();
39
- if (!fs) {
42
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
43
+ if (!_namespace || !fs) {
40
44
  throw Error('FullStory is not loaded, please ensure the init function is invoked before calling FullStory API functions');
41
45
  }
42
46
  return fs;
@@ -44,30 +48,36 @@ var ensureSnippetLoaded = function () {
44
48
  var _init = function (inputOptions, readyCallback) {
45
49
  // Make a copy so we can modify `options` if desired.
46
50
  var options = __assign({}, inputOptions);
47
- if (getFullStory()) {
51
+ var ns = options.namespace || 'FS';
52
+ _namespace = ns;
53
+ if (getFullStory(ns)) {
48
54
  console.warn('The FullStory snippet has already been defined elsewhere (likely in the <head> element)');
49
55
  return;
50
56
  }
57
+ var fsInitEnv = {};
51
58
  // see README for details on the recordCrossDomainIFrames option
52
59
  if (options.recordCrossDomainIFrames) {
53
- window._fs_run_in_iframe = true;
60
+ fsInitEnv.runInIframe = true;
54
61
  }
55
62
  if (options.appHost) {
56
- window._fs_app_host = options.appHost;
63
+ fsInitEnv.appHost = options.appHost;
57
64
  }
58
65
  if (options.assetMapId) {
59
- window._fs_asset_map_id = options.assetMapId;
66
+ fsInitEnv.assetMapId = options.assetMapId;
60
67
  }
61
68
  if (options.startCaptureManually) {
62
- window._fs_capture_on_startup = false;
69
+ fsInitEnv.captureOnStartup = false;
63
70
  }
64
71
  // record the contents of this iFrame when embedded in a parent site
65
72
  if (options.recordOnlyThisIFrame) {
66
- window._fs_is_outer_script = true;
73
+ fsInitEnv.isOuterScript = true;
67
74
  }
68
75
  // Set cookie domain if it was specified.
69
76
  if (options.cookieDomain) {
70
- window._fs_cookie_domain = options.cookieDomain;
77
+ fsInitEnv.cookieDomain = options.cookieDomain;
78
+ }
79
+ if (options.sessionUid) {
80
+ fsInitEnv.sessionUid = options.sessionUid;
71
81
  }
72
82
  if (options.debug === true) {
73
83
  if (!options.script) {
@@ -78,14 +88,12 @@ var _init = function (inputOptions, readyCallback) {
78
88
  }
79
89
  }
80
90
  initFS(options);
81
- var fs = getFullStory();
91
+ var fs = getFullStory(_namespace);
82
92
  if (!fs) {
83
93
  console.warn('Failed to initialize FS snippet');
84
94
  return;
85
95
  }
86
- if (options.sessionUid) {
87
- fs('init', { env: { sessionUid: options.sessionUid } });
88
- }
96
+ fs('init', { env: fsInitEnv });
89
97
  if (readyCallback) {
90
98
  fs('observe', { type: 'start', callback: readyCallback });
91
99
  }
@@ -98,22 +106,21 @@ var _init = function (inputOptions, readyCallback) {
98
106
  }
99
107
  });
100
108
  fs('shutdown');
101
- window._fs_dev_mode = true;
109
+ _isDevMode = true;
102
110
  console.warn(message);
103
111
  }
104
112
  };
105
113
  var initOnce = function (message) { return function (inputOptions, readyCallback) {
106
- if (window._fs_initialized) {
114
+ if (_initialized) {
107
115
  if (message)
108
116
  console.warn(message);
109
117
  return;
110
118
  }
111
119
  _init(inputOptions, readyCallback);
112
- window._fs_initialized = true;
120
+ _initialized = true;
113
121
  }; };
114
122
  var init = initOnce('FullStory init has already been called once, additional invocations are ignored');
115
- // normalize undefined into boolean
116
- var isInitialized = function () { return !!window._fs_initialized; };
123
+ var isInitialized = function () { return _initialized; };
117
124
  var hasFullStoryWithFunction = function () {
118
125
  var testNames = [];
119
126
  for (var _i = 0; _i < arguments.length; _i++) {
@@ -127,12 +134,12 @@ var guard = function (name) { return function () {
127
134
  for (var _i = 0; _i < arguments.length; _i++) {
128
135
  args[_i] = arguments[_i];
129
136
  }
130
- if (window._fs_dev_mode) {
137
+ if (_isDevMode) {
131
138
  var message = "FullStory is in dev mode and is not capturing: ".concat(name, " method not executed");
132
139
  console.warn(message);
133
140
  return message;
134
141
  }
135
- var fs = getFullStory();
142
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
136
143
  if (hasFullStoryWithFunction(name) && fs) {
137
144
  return fs[name].apply(fs, args);
138
145
  }
@@ -142,7 +149,7 @@ var guard = function (name) { return function () {
142
149
  var buildFullStoryShim = function () {
143
150
  var FS = function (operation, options, source) {
144
151
  var fs = ensureSnippetLoaded();
145
- if (window._fs_dev_mode) {
152
+ if (_isDevMode) {
146
153
  var message = "FullStory is in dev mode and is not capturing: ".concat(operation, " not executed");
147
154
  console.warn(message);
148
155
  return undefined;
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
16
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
17
  PERFORMANCE OF THIS SOFTWARE.
18
18
  ***************************************************************************** */
19
- /* global Reflect, Promise */
19
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
20
20
 
21
21
 
22
22
  var __assign = function() {
@@ -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,30 +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;
56
63
  }
57
64
  if (options.appHost) {
58
- window._fs_app_host = options.appHost;
65
+ fsInitEnv.appHost = options.appHost;
59
66
  }
60
67
  if (options.assetMapId) {
61
- window._fs_asset_map_id = options.assetMapId;
68
+ fsInitEnv.assetMapId = options.assetMapId;
62
69
  }
63
70
  if (options.startCaptureManually) {
64
- window._fs_capture_on_startup = false;
71
+ fsInitEnv.captureOnStartup = false;
65
72
  }
66
73
  // record the contents of this iFrame when embedded in a parent site
67
74
  if (options.recordOnlyThisIFrame) {
68
- window._fs_is_outer_script = true;
75
+ fsInitEnv.isOuterScript = true;
69
76
  }
70
77
  // Set cookie domain if it was specified.
71
78
  if (options.cookieDomain) {
72
- window._fs_cookie_domain = options.cookieDomain;
79
+ fsInitEnv.cookieDomain = options.cookieDomain;
80
+ }
81
+ if (options.sessionUid) {
82
+ fsInitEnv.sessionUid = options.sessionUid;
73
83
  }
74
84
  if (options.debug === true) {
75
85
  if (!options.script) {
@@ -80,14 +90,12 @@ var _init = function (inputOptions, readyCallback) {
80
90
  }
81
91
  }
82
92
  snippet.initFS(options);
83
- var fs = getFullStory();
93
+ var fs = getFullStory(_namespace);
84
94
  if (!fs) {
85
95
  console.warn('Failed to initialize FS snippet');
86
96
  return;
87
97
  }
88
- if (options.sessionUid) {
89
- fs('init', { env: { sessionUid: options.sessionUid } });
90
- }
98
+ fs('init', { env: fsInitEnv });
91
99
  if (readyCallback) {
92
100
  fs('observe', { type: 'start', callback: readyCallback });
93
101
  }
@@ -100,22 +108,21 @@ var _init = function (inputOptions, readyCallback) {
100
108
  }
101
109
  });
102
110
  fs('shutdown');
103
- window._fs_dev_mode = true;
111
+ _isDevMode = true;
104
112
  console.warn(message);
105
113
  }
106
114
  };
107
115
  var initOnce = function (message) { return function (inputOptions, readyCallback) {
108
- if (window._fs_initialized) {
116
+ if (_initialized) {
109
117
  if (message)
110
118
  console.warn(message);
111
119
  return;
112
120
  }
113
121
  _init(inputOptions, readyCallback);
114
- window._fs_initialized = true;
122
+ _initialized = true;
115
123
  }; };
116
124
  var init = initOnce('FullStory init has already been called once, additional invocations are ignored');
117
- // normalize undefined into boolean
118
- var isInitialized = function () { return !!window._fs_initialized; };
125
+ var isInitialized = function () { return _initialized; };
119
126
  var hasFullStoryWithFunction = function () {
120
127
  var testNames = [];
121
128
  for (var _i = 0; _i < arguments.length; _i++) {
@@ -129,12 +136,12 @@ var guard = function (name) { return function () {
129
136
  for (var _i = 0; _i < arguments.length; _i++) {
130
137
  args[_i] = arguments[_i];
131
138
  }
132
- if (window._fs_dev_mode) {
139
+ if (_isDevMode) {
133
140
  var message = "FullStory is in dev mode and is not capturing: ".concat(name, " method not executed");
134
141
  console.warn(message);
135
142
  return message;
136
143
  }
137
- var fs = getFullStory();
144
+ var fs = _namespace ? getFullStory(_namespace) : undefined;
138
145
  if (hasFullStoryWithFunction(name) && fs) {
139
146
  return fs[name].apply(fs, args);
140
147
  }
@@ -144,7 +151,7 @@ var guard = function (name) { return function () {
144
151
  var buildFullStoryShim = function () {
145
152
  var FS = function (operation, options, source) {
146
153
  var fs = ensureSnippetLoaded();
147
- if (window._fs_dev_mode) {
154
+ if (_isDevMode) {
148
155
  var message = "FullStory is in dev mode and is not capturing: ".concat(operation, " not executed");
149
156
  console.warn(message);
150
157
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstory/browser",
3
- "version": "2.0.8",
3
+ "version": "2.1.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"
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
162
  const init = initOnce('FullStory init has already been called once, additional invocations are ignored');
161
163
 
162
- // normalize undefined into boolean
163
- 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;