@fullstory/browser 2.1.0 → 2.1.1
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 +13 -2
- package/dist/hosts.d.ts +36 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.esm.js +94 -0
- package/dist/index.js +94 -0
- package/package.json +1 -1
- package/src/hosts.test.ts +65 -0
- package/src/hosts.ts +94 -0
- package/src/index.test.ts +74 -0
- package/src/index.ts +19 -2
package/README.md
CHANGED
|
@@ -73,8 +73,8 @@ The only required option is `orgId`, all others are optional.
|
|
|
73
73
|
|
|
74
74
|
* `orgId` - Sets your Fullstory Org Id. Find out how to get your Org Id [here](https://help.fullstory.com/hc/en-us/articles/360047075853).
|
|
75
75
|
* `debug` - When set to `true`, enables Fullstory debug messages; defaults to `false`.
|
|
76
|
-
* `host` - The recording server host domain. Can be set to direct recorded events to a proxy that you host. Defaults to `fullstory.com
|
|
77
|
-
* `script` - Fullstory script host domain. Fullstory hosts the `fs.js` recording script on a CDN, but you can choose to host a copy yourself. Defaults to `edge.fullstory.com
|
|
76
|
+
* `host` - The recording server host domain. Can be set to direct recorded events to a proxy that you host. Defaults to `fullstory.com`, or to the region-specific equivalent (e.g. `eu1.fullstory.com`) when your `orgId` carries a region suffix.
|
|
77
|
+
* `script` - Fullstory script host domain. Fullstory hosts the `fs.js` recording script on a CDN, but you can choose to host a copy yourself. Defaults to `edge.fullstory.com/s/fs.js`, or to the region-specific equivalent (e.g. `edge.eu1.fullstory.com/s/fs.js`) when your `orgId` carries a region suffix.
|
|
78
78
|
* `namespace` - Sets the global identifier for Fullstory when conflicts with `FS` arise; see [help](https://help.fullstory.com/hc/en-us/articles/360020624694-What-if-the-identifier-FS-is-used-by-another-script-on-my-site-).
|
|
79
79
|
* `cookieDomain` - Overrides the cookie domain. By default, cookies will be valid for all subdomains of your site; if you want to limit the cookies to a specific subdomain, you can set the domain value explicitly. More information can be found [here](https://help.fullstory.com/hc/en-us/articles/360020622874-Can-the-Fullstory-cookie-be-associated-with-a-specific-subdomain-).
|
|
80
80
|
* `recordCrossDomainIFrames` - Defaults to `false`. Fullstory can record cross-domain iFrames if: 1. The Fullstory Browser SDK is running in the cross-domain iFrame and 2. `recordCrossDomainIFrames` is set to `true` in the cross-domain iFrame and 3. The Fullstory Browser SDK is running in the parent page of the cross-domain iFrame. Click [here](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) for a detailed explanation of what "cross-domain" means. Before using, you should understand the security implications, and configure your [Content Security Policy](https://www.html5rocks.com/en/tutorials/security/content-security-policy/) (CSP) HTTP headers accordingly - specifically the frame-ancestors directive. Failure to configure your CSP headers while using this setting can bypass IFrames security protections that are included in modern browsers. More information about cross-domain iFrame recording can be found on our [Knowledge Base](https://help.fullstory.com/hc/en-us/articles/360020622514-Can-Fullstory-capture-content-that-is-presented-in-iframes-#2-the-outer-page-is-running-fullstory-and-you-have-iframes-runni). Note: the `recordCrossDomainIFrames` parameter is the same as the `window['_fs_run_in_iframe']` referenced in the KB article.
|
|
@@ -84,6 +84,17 @@ The only required option is `orgId`, all others are optional.
|
|
|
84
84
|
* `assetMapId` - Use this to set the current asset map id. See [Asset Uploading for Web](https://help.fullstory.com/hc/en-us/articles/4404129191575-Asset-Uploading-for-Web) for more information.
|
|
85
85
|
* `appHost` - Use this to set the app host for displaying session urls. If using a version of [Fullstory Relay](https://help.fullstory.com/hc/en-us/articles/360046112593-How-to-send-captured-traffic-to-your-First-Party-Domain-using-Fullstory-Relay), you may need to set `appHost` "app.fullstory.com" or "app.eu1.fullstory.com" depending on your region.
|
|
86
86
|
|
|
87
|
+
### Data center regions
|
|
88
|
+
|
|
89
|
+
Your `orgId` encodes the data center your org lives in — for example the `eu1` in `o-ABC123-eu1`. The SDK reads that suffix and points `host`, `script` and `appHost` at the matching region, so an org outside the default (`na1`) data center does not need to configure anything beyond `orgId`:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
FullStory.init({ orgId: 'o-ABC123-eu1' });
|
|
93
|
+
// loads https://edge.eu1.fullstory.com/s/fs.js and records to eu1.fullstory.com
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The region is applied to any of those three options that resolve to a Fullstory-owned domain, **including values you pass explicitly** — so `host: "fullstory.com"` on an `eu1` org becomes `eu1.fullstory.com` rather than silently recording to the wrong data center. Hosts Fullstory does not own, such as a proxy or Fullstory Relay domain of your own, are always used exactly as you provide them.
|
|
97
|
+
|
|
87
98
|
### Ready Callback
|
|
88
99
|
|
|
89
100
|
The `init` function also accepts an optional `readyCallback` argument. If you provide a function, it will be invoked when the Fullstory session has started. Your callback will be called with one parameter: an object containing information about the session. Currently the only property is `sessionUrl`, which is a string containing the URL to the session.
|
package/dist/hosts.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region resolution for the Fullstory host/script options.
|
|
3
|
+
*
|
|
4
|
+
* `fs.js` derives an org's region by parsing the locale suffix off its orgId (see
|
|
5
|
+
* `orgLocale()` / `fullstoryHost()` in the monorepo's `packages/recording/src/fsglobals.ts`),
|
|
6
|
+
* but that only runs once `fs.js` has already loaded — far too late to fix the URL that
|
|
7
|
+
* fetched it. So the same resolution has to happen here, before the snippet injects its
|
|
8
|
+
* `<script>` tag, or an EU org that sets nothing but `orgId` fetches `fs.js` and its
|
|
9
|
+
* settings from the na1 edge, which does not serve that org (VAL-10545).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The `host` / `script` defaults applied by `@fullstory/snippet`. Duplicated here because
|
|
13
|
+
* the package does not export them, and we need the default value in hand to regionalize
|
|
14
|
+
* it. Keep in sync with the defaults in that package's `initFS` / `executeSnippet` when
|
|
15
|
+
* upgrading the dependency.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_HOST = "fullstory.com";
|
|
18
|
+
export declare const DEFAULT_SCRIPT = "edge.fullstory.com/s/fs.js";
|
|
19
|
+
/**
|
|
20
|
+
* Parses the region/locale suffix out of an orgId, e.g. "o-7Y9H-eu1" -> "eu1". Returns
|
|
21
|
+
* `undefined` for na1 and for orgIds that carry no region, meaning "apply no region".
|
|
22
|
+
* Mirrors `orgLocale()` in the monorepo's `packages/recording/src/fsglobals.ts`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const orgLocale: (orgId?: string) => string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Applies an org's region to a Fullstory-owned host, inserting the locale as the label
|
|
27
|
+
* immediately before the registrable domain and preserving any path:
|
|
28
|
+
*
|
|
29
|
+
* ("fullstory.com", "eu1") -> "eu1.fullstory.com"
|
|
30
|
+
* ("edge.fullstory.com/s/fs.js", "eu1") -> "edge.eu1.fullstory.com/s/fs.js"
|
|
31
|
+
*
|
|
32
|
+
* Hosts Fullstory does not own — self-hosted proxies, Relay, localhost — are returned
|
|
33
|
+
* untouched, as are hosts already carrying the right region (so this is idempotent and
|
|
34
|
+
* safe to stack with the same resolution in `fs.js`).
|
|
35
|
+
*/
|
|
36
|
+
export declare const regionalize: (value: string, locale?: string) => string;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,13 +5,17 @@ import { FSApi } from '@fullstory/snippet';
|
|
|
5
5
|
* - orgId: Reference for your [Org Id](https://help.fullstory.com/hc/en-us/articles/360047075853) listed in FullStory.
|
|
6
6
|
* - namespace: Global object name that contains the FullStory browser API methods and properties. Defaults to `FS`.
|
|
7
7
|
* - debug: Debug mode with extra browser console logging.
|
|
8
|
-
* - host: The recording server host domain. Can be set to direct recorded events to a proxy that you host. Defaults to `fullstory.com
|
|
9
|
-
* - script: FullStory script host domain. FullStory hosts the `fs.js` recording script on a CDN, but you can choose to host a copy yourself. Defaults to `edge.fullstory.com
|
|
8
|
+
* - host: The recording server host domain. Can be set to direct recorded events to a proxy that you host. Defaults to `fullstory.com`, or the region-specific equivalent (e.g. `eu1.fullstory.com`) when `orgId` carries a region suffix.
|
|
9
|
+
* - script: FullStory script host domain. FullStory hosts the `fs.js` recording script on a CDN, but you can choose to host a copy yourself. Defaults to `edge.fullstory.com/s/fs.js`, or the region-specific equivalent (e.g. `edge.eu1.fullstory.com/s/fs.js`) when `orgId` carries a region suffix.
|
|
10
10
|
* - cookieDomain: Overrides the cookie domain. By default, cookies will be valid for all subdomains of your site; if you want to limit the cookies to a specific subdomain, you can set the domain value explicitly. More information can be found [here](https://help.fullstory.com/hc/en-us/articles/360020622874-Can-the-FullStory-cookie-be-associated-with-a-specific-subdomain-).
|
|
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
14
|
* - sessionUid (beta): Sets the session UID passed to the `FS('init', ...)` v2 operation.
|
|
15
|
+
*
|
|
16
|
+
* The region encoded in `orgId` (e.g. the `eu1` in `o-ABC123-eu1`) is applied to `host`, `script` and `appHost`
|
|
17
|
+
* whenever those resolve to a FullStory-owned domain, including values you set explicitly. Hosts FullStory does
|
|
18
|
+
* not own — a proxy or Relay domain of your own — are always used exactly as given.
|
|
15
19
|
*/
|
|
16
20
|
export interface SnippetOptions {
|
|
17
21
|
orgId: string;
|
package/dist/index.esm.js
CHANGED
|
@@ -33,6 +33,89 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
33
33
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Region resolution for the Fullstory host/script options.
|
|
38
|
+
*
|
|
39
|
+
* `fs.js` derives an org's region by parsing the locale suffix off its orgId (see
|
|
40
|
+
* `orgLocale()` / `fullstoryHost()` in the monorepo's `packages/recording/src/fsglobals.ts`),
|
|
41
|
+
* but that only runs once `fs.js` has already loaded — far too late to fix the URL that
|
|
42
|
+
* fetched it. So the same resolution has to happen here, before the snippet injects its
|
|
43
|
+
* `<script>` tag, or an EU org that sets nothing but `orgId` fetches `fs.js` and its
|
|
44
|
+
* settings from the na1 edge, which does not serve that org (VAL-10545).
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* The `host` / `script` defaults applied by `@fullstory/snippet`. Duplicated here because
|
|
48
|
+
* the package does not export them, and we need the default value in hand to regionalize
|
|
49
|
+
* it. Keep in sync with the defaults in that package's `initFS` / `executeSnippet` when
|
|
50
|
+
* upgrading the dependency.
|
|
51
|
+
*/
|
|
52
|
+
var DEFAULT_HOST = 'fullstory.com';
|
|
53
|
+
var DEFAULT_SCRIPT = 'edge.fullstory.com/s/fs.js';
|
|
54
|
+
/**
|
|
55
|
+
* Matches any subdomain of "fullstory.com", e.g. "www.fullstory.com", "edge.fullstory.com".
|
|
56
|
+
* Anchored at the start, so a customer proxy such as "cdn.acme.com/fullstory.com/x" does
|
|
57
|
+
* not match.
|
|
58
|
+
*/
|
|
59
|
+
var FS_DOMAIN = /^([^.]+\.)*fullstory\.com(\/|$)/;
|
|
60
|
+
/** Region labels look like "eu1", "ap1", "na1". */
|
|
61
|
+
var REGION_LABEL = /^[a-z]{2,3}[0-9]+$/;
|
|
62
|
+
/**
|
|
63
|
+
* Parses the region/locale suffix out of an orgId, e.g. "o-7Y9H-eu1" -> "eu1". Returns
|
|
64
|
+
* `undefined` for na1 and for orgIds that carry no region, meaning "apply no region".
|
|
65
|
+
* Mirrors `orgLocale()` in the monorepo's `packages/recording/src/fsglobals.ts`.
|
|
66
|
+
*/
|
|
67
|
+
var orgLocale = function (orgId) {
|
|
68
|
+
var sections = orgId ? orgId.split('-') : [];
|
|
69
|
+
if (sections.length < 3) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
var prefix = sections[0];
|
|
73
|
+
var realm = sections[sections.length - 1];
|
|
74
|
+
// A standard type prefix has only one character in it (o, u, p, etc.). If there is more
|
|
75
|
+
// than one character then we most likely have a legacy org ID that happens to contain
|
|
76
|
+
// hyphens, not a region suffix.
|
|
77
|
+
if (realm === 'na1' || prefix.length > 1) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return realm;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Applies an org's region to a Fullstory-owned host, inserting the locale as the label
|
|
84
|
+
* immediately before the registrable domain and preserving any path:
|
|
85
|
+
*
|
|
86
|
+
* ("fullstory.com", "eu1") -> "eu1.fullstory.com"
|
|
87
|
+
* ("edge.fullstory.com/s/fs.js", "eu1") -> "edge.eu1.fullstory.com/s/fs.js"
|
|
88
|
+
*
|
|
89
|
+
* Hosts Fullstory does not own — self-hosted proxies, Relay, localhost — are returned
|
|
90
|
+
* untouched, as are hosts already carrying the right region (so this is idempotent and
|
|
91
|
+
* safe to stack with the same resolution in `fs.js`).
|
|
92
|
+
*/
|
|
93
|
+
var regionalize = function (value, locale) {
|
|
94
|
+
if (!locale || !FS_DOMAIN.test(value)) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
// Only the host is rewritten; anything from the first "/" on is left alone.
|
|
98
|
+
var pathIndex = value.indexOf('/');
|
|
99
|
+
var host = pathIndex === -1 ? value : value.slice(0, pathIndex);
|
|
100
|
+
var path = pathIndex === -1 ? '' : value.slice(pathIndex);
|
|
101
|
+
var labels = host.split('.');
|
|
102
|
+
// FS_DOMAIN guarantees at least "fullstory.com", so this is never negative.
|
|
103
|
+
var insertAt = labels.length - 2;
|
|
104
|
+
var existing = insertAt > 0 ? labels[insertAt - 1] : undefined;
|
|
105
|
+
if (existing === locale) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
if (existing !== undefined && REGION_LABEL.test(existing)) {
|
|
109
|
+
// A different region is already present (e.g. "edge.na1.fullstory.com" on an eu1 org).
|
|
110
|
+
// Replace it rather than insert, which would yield "edge.eu1.na1.fullstory.com".
|
|
111
|
+
labels[insertAt - 1] = locale;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
labels.splice(insertAt, 0, locale);
|
|
115
|
+
}
|
|
116
|
+
return labels.join('.') + path;
|
|
117
|
+
};
|
|
118
|
+
|
|
36
119
|
// Module-level state set during `_init` (replaces former window `_fs_*` globals).
|
|
37
120
|
var _namespace;
|
|
38
121
|
var _initialized = false;
|
|
@@ -87,6 +170,17 @@ var _init = function (inputOptions, readyCallback) {
|
|
|
87
170
|
console.warn('Ignoring `debug = true` because `script` is set');
|
|
88
171
|
}
|
|
89
172
|
}
|
|
173
|
+
// `fs.js` resolves the org's region itself, but not until after it has loaded — the URL
|
|
174
|
+
// that fetches it has to be regionalized here instead (VAL-10545). Fullstory-owned hosts
|
|
175
|
+
// pick up the region label; customer proxies and Relay hosts are left alone.
|
|
176
|
+
var locale = orgLocale(options.orgId);
|
|
177
|
+
if (locale) {
|
|
178
|
+
options.host = regionalize(options.host || DEFAULT_HOST, locale);
|
|
179
|
+
options.script = regionalize(options.script || DEFAULT_SCRIPT, locale);
|
|
180
|
+
if (fsInitEnv.appHost) {
|
|
181
|
+
fsInitEnv.appHost = regionalize(fsInitEnv.appHost, locale);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
90
184
|
initFS(options);
|
|
91
185
|
var fs = getFullStory(_namespace);
|
|
92
186
|
if (!fs) {
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,89 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
35
35
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Region resolution for the Fullstory host/script options.
|
|
40
|
+
*
|
|
41
|
+
* `fs.js` derives an org's region by parsing the locale suffix off its orgId (see
|
|
42
|
+
* `orgLocale()` / `fullstoryHost()` in the monorepo's `packages/recording/src/fsglobals.ts`),
|
|
43
|
+
* but that only runs once `fs.js` has already loaded — far too late to fix the URL that
|
|
44
|
+
* fetched it. So the same resolution has to happen here, before the snippet injects its
|
|
45
|
+
* `<script>` tag, or an EU org that sets nothing but `orgId` fetches `fs.js` and its
|
|
46
|
+
* settings from the na1 edge, which does not serve that org (VAL-10545).
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* The `host` / `script` defaults applied by `@fullstory/snippet`. Duplicated here because
|
|
50
|
+
* the package does not export them, and we need the default value in hand to regionalize
|
|
51
|
+
* it. Keep in sync with the defaults in that package's `initFS` / `executeSnippet` when
|
|
52
|
+
* upgrading the dependency.
|
|
53
|
+
*/
|
|
54
|
+
var DEFAULT_HOST = 'fullstory.com';
|
|
55
|
+
var DEFAULT_SCRIPT = 'edge.fullstory.com/s/fs.js';
|
|
56
|
+
/**
|
|
57
|
+
* Matches any subdomain of "fullstory.com", e.g. "www.fullstory.com", "edge.fullstory.com".
|
|
58
|
+
* Anchored at the start, so a customer proxy such as "cdn.acme.com/fullstory.com/x" does
|
|
59
|
+
* not match.
|
|
60
|
+
*/
|
|
61
|
+
var FS_DOMAIN = /^([^.]+\.)*fullstory\.com(\/|$)/;
|
|
62
|
+
/** Region labels look like "eu1", "ap1", "na1". */
|
|
63
|
+
var REGION_LABEL = /^[a-z]{2,3}[0-9]+$/;
|
|
64
|
+
/**
|
|
65
|
+
* Parses the region/locale suffix out of an orgId, e.g. "o-7Y9H-eu1" -> "eu1". Returns
|
|
66
|
+
* `undefined` for na1 and for orgIds that carry no region, meaning "apply no region".
|
|
67
|
+
* Mirrors `orgLocale()` in the monorepo's `packages/recording/src/fsglobals.ts`.
|
|
68
|
+
*/
|
|
69
|
+
var orgLocale = function (orgId) {
|
|
70
|
+
var sections = orgId ? orgId.split('-') : [];
|
|
71
|
+
if (sections.length < 3) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
var prefix = sections[0];
|
|
75
|
+
var realm = sections[sections.length - 1];
|
|
76
|
+
// A standard type prefix has only one character in it (o, u, p, etc.). If there is more
|
|
77
|
+
// than one character then we most likely have a legacy org ID that happens to contain
|
|
78
|
+
// hyphens, not a region suffix.
|
|
79
|
+
if (realm === 'na1' || prefix.length > 1) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return realm;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Applies an org's region to a Fullstory-owned host, inserting the locale as the label
|
|
86
|
+
* immediately before the registrable domain and preserving any path:
|
|
87
|
+
*
|
|
88
|
+
* ("fullstory.com", "eu1") -> "eu1.fullstory.com"
|
|
89
|
+
* ("edge.fullstory.com/s/fs.js", "eu1") -> "edge.eu1.fullstory.com/s/fs.js"
|
|
90
|
+
*
|
|
91
|
+
* Hosts Fullstory does not own — self-hosted proxies, Relay, localhost — are returned
|
|
92
|
+
* untouched, as are hosts already carrying the right region (so this is idempotent and
|
|
93
|
+
* safe to stack with the same resolution in `fs.js`).
|
|
94
|
+
*/
|
|
95
|
+
var regionalize = function (value, locale) {
|
|
96
|
+
if (!locale || !FS_DOMAIN.test(value)) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
// Only the host is rewritten; anything from the first "/" on is left alone.
|
|
100
|
+
var pathIndex = value.indexOf('/');
|
|
101
|
+
var host = pathIndex === -1 ? value : value.slice(0, pathIndex);
|
|
102
|
+
var path = pathIndex === -1 ? '' : value.slice(pathIndex);
|
|
103
|
+
var labels = host.split('.');
|
|
104
|
+
// FS_DOMAIN guarantees at least "fullstory.com", so this is never negative.
|
|
105
|
+
var insertAt = labels.length - 2;
|
|
106
|
+
var existing = insertAt > 0 ? labels[insertAt - 1] : undefined;
|
|
107
|
+
if (existing === locale) {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
if (existing !== undefined && REGION_LABEL.test(existing)) {
|
|
111
|
+
// A different region is already present (e.g. "edge.na1.fullstory.com" on an eu1 org).
|
|
112
|
+
// Replace it rather than insert, which would yield "edge.eu1.na1.fullstory.com".
|
|
113
|
+
labels[insertAt - 1] = locale;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
labels.splice(insertAt, 0, locale);
|
|
117
|
+
}
|
|
118
|
+
return labels.join('.') + path;
|
|
119
|
+
};
|
|
120
|
+
|
|
38
121
|
// Module-level state set during `_init` (replaces former window `_fs_*` globals).
|
|
39
122
|
var _namespace;
|
|
40
123
|
var _initialized = false;
|
|
@@ -89,6 +172,17 @@ var _init = function (inputOptions, readyCallback) {
|
|
|
89
172
|
console.warn('Ignoring `debug = true` because `script` is set');
|
|
90
173
|
}
|
|
91
174
|
}
|
|
175
|
+
// `fs.js` resolves the org's region itself, but not until after it has loaded — the URL
|
|
176
|
+
// that fetches it has to be regionalized here instead (VAL-10545). Fullstory-owned hosts
|
|
177
|
+
// pick up the region label; customer proxies and Relay hosts are left alone.
|
|
178
|
+
var locale = orgLocale(options.orgId);
|
|
179
|
+
if (locale) {
|
|
180
|
+
options.host = regionalize(options.host || DEFAULT_HOST, locale);
|
|
181
|
+
options.script = regionalize(options.script || DEFAULT_SCRIPT, locale);
|
|
182
|
+
if (fsInitEnv.appHost) {
|
|
183
|
+
fsInitEnv.appHost = regionalize(fsInitEnv.appHost, locale);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
92
186
|
snippet.initFS(options);
|
|
93
187
|
var fs = getFullStory(_namespace);
|
|
94
188
|
if (!fs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstory/browser",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DEFAULT_HOST, DEFAULT_SCRIPT, orgLocale, regionalize } from './hosts';
|
|
2
|
+
|
|
3
|
+
describe('orgLocale', () => {
|
|
4
|
+
it('should return the region suffix of a regional orgId', () => {
|
|
5
|
+
expect(orgLocale('o-7Y9H-eu1')).toBe('eu1');
|
|
6
|
+
expect(orgLocale('o-1A2B3C-ap1')).toBe('ap1');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should return undefined for na1, which needs no region label', () => {
|
|
10
|
+
expect(orgLocale('o-7Y9H-na1')).toBeUndefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return undefined for orgIds with no region', () => {
|
|
14
|
+
expect(orgLocale('123')).toBeUndefined();
|
|
15
|
+
expect(orgLocale('o-7Y9H')).toBeUndefined();
|
|
16
|
+
expect(orgLocale('')).toBeUndefined();
|
|
17
|
+
expect(orgLocale(undefined)).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should ignore legacy orgIds that merely contain hyphens', () => {
|
|
21
|
+
// A real type prefix is a single character (o, u, p); anything longer is a legacy id.
|
|
22
|
+
expect(orgLocale('ACME-Widgets-Inc')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('regionalize', () => {
|
|
27
|
+
it('should insert the region before the registrable domain', () => {
|
|
28
|
+
expect(regionalize(DEFAULT_HOST, 'eu1')).toBe('eu1.fullstory.com');
|
|
29
|
+
expect(regionalize(DEFAULT_SCRIPT, 'eu1')).toBe('edge.eu1.fullstory.com/s/fs.js');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should preserve the path of a script URL', () => {
|
|
33
|
+
expect(regionalize('edge.fullstory.com/s/fs-debug.js', 'eu1'))
|
|
34
|
+
.toBe('edge.eu1.fullstory.com/s/fs-debug.js');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should regionalize the www and app subdomains', () => {
|
|
38
|
+
expect(regionalize('www.fullstory.com', 'eu1')).toBe('www.eu1.fullstory.com');
|
|
39
|
+
expect(regionalize('app.fullstory.com', 'eu1')).toBe('app.eu1.fullstory.com');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should be idempotent for hosts already carrying the region', () => {
|
|
43
|
+
expect(regionalize('eu1.fullstory.com', 'eu1')).toBe('eu1.fullstory.com');
|
|
44
|
+
expect(regionalize('edge.eu1.fullstory.com/s/fs.js', 'eu1'))
|
|
45
|
+
.toBe('edge.eu1.fullstory.com/s/fs.js');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should replace a mismatched region rather than stacking another one on top', () => {
|
|
49
|
+
expect(regionalize('edge.na1.fullstory.com/s/fs.js', 'eu1'))
|
|
50
|
+
.toBe('edge.eu1.fullstory.com/s/fs.js');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should leave hosts Fullstory does not own untouched', () => {
|
|
54
|
+
expect(regionalize('fs.acme.com', 'eu1')).toBe('fs.acme.com');
|
|
55
|
+
expect(regionalize('localhost:8080', 'eu1')).toBe('localhost:8080');
|
|
56
|
+
// Anchored, so a proxy path that happens to mention fullstory.com is not a match.
|
|
57
|
+
expect(regionalize('cdn.acme.com/fullstory.com/fs.js', 'eu1'))
|
|
58
|
+
.toBe('cdn.acme.com/fullstory.com/fs.js');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return the value unchanged when there is no region', () => {
|
|
62
|
+
expect(regionalize(DEFAULT_SCRIPT, undefined)).toBe(DEFAULT_SCRIPT);
|
|
63
|
+
expect(regionalize(DEFAULT_SCRIPT, '')).toBe(DEFAULT_SCRIPT);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/hosts.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region resolution for the Fullstory host/script options.
|
|
3
|
+
*
|
|
4
|
+
* `fs.js` derives an org's region by parsing the locale suffix off its orgId (see
|
|
5
|
+
* `orgLocale()` / `fullstoryHost()` in the monorepo's `packages/recording/src/fsglobals.ts`),
|
|
6
|
+
* but that only runs once `fs.js` has already loaded — far too late to fix the URL that
|
|
7
|
+
* fetched it. So the same resolution has to happen here, before the snippet injects its
|
|
8
|
+
* `<script>` tag, or an EU org that sets nothing but `orgId` fetches `fs.js` and its
|
|
9
|
+
* settings from the na1 edge, which does not serve that org (VAL-10545).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The `host` / `script` defaults applied by `@fullstory/snippet`. Duplicated here because
|
|
14
|
+
* the package does not export them, and we need the default value in hand to regionalize
|
|
15
|
+
* it. Keep in sync with the defaults in that package's `initFS` / `executeSnippet` when
|
|
16
|
+
* upgrading the dependency.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_HOST = 'fullstory.com';
|
|
19
|
+
export const DEFAULT_SCRIPT = 'edge.fullstory.com/s/fs.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Matches any subdomain of "fullstory.com", e.g. "www.fullstory.com", "edge.fullstory.com".
|
|
23
|
+
* Anchored at the start, so a customer proxy such as "cdn.acme.com/fullstory.com/x" does
|
|
24
|
+
* not match.
|
|
25
|
+
*/
|
|
26
|
+
const FS_DOMAIN = /^([^.]+\.)*fullstory\.com(\/|$)/;
|
|
27
|
+
|
|
28
|
+
/** Region labels look like "eu1", "ap1", "na1". */
|
|
29
|
+
const REGION_LABEL = /^[a-z]{2,3}[0-9]+$/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses the region/locale suffix out of an orgId, e.g. "o-7Y9H-eu1" -> "eu1". Returns
|
|
33
|
+
* `undefined` for na1 and for orgIds that carry no region, meaning "apply no region".
|
|
34
|
+
* Mirrors `orgLocale()` in the monorepo's `packages/recording/src/fsglobals.ts`.
|
|
35
|
+
*/
|
|
36
|
+
export const orgLocale = (orgId?: string): string | undefined => {
|
|
37
|
+
const sections = orgId ? orgId.split('-') : [];
|
|
38
|
+
if (sections.length < 3) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const prefix = sections[0];
|
|
43
|
+
const realm = sections[sections.length - 1];
|
|
44
|
+
|
|
45
|
+
// A standard type prefix has only one character in it (o, u, p, etc.). If there is more
|
|
46
|
+
// than one character then we most likely have a legacy org ID that happens to contain
|
|
47
|
+
// hyphens, not a region suffix.
|
|
48
|
+
if (realm === 'na1' || prefix.length > 1) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return realm;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Applies an org's region to a Fullstory-owned host, inserting the locale as the label
|
|
57
|
+
* immediately before the registrable domain and preserving any path:
|
|
58
|
+
*
|
|
59
|
+
* ("fullstory.com", "eu1") -> "eu1.fullstory.com"
|
|
60
|
+
* ("edge.fullstory.com/s/fs.js", "eu1") -> "edge.eu1.fullstory.com/s/fs.js"
|
|
61
|
+
*
|
|
62
|
+
* Hosts Fullstory does not own — self-hosted proxies, Relay, localhost — are returned
|
|
63
|
+
* untouched, as are hosts already carrying the right region (so this is idempotent and
|
|
64
|
+
* safe to stack with the same resolution in `fs.js`).
|
|
65
|
+
*/
|
|
66
|
+
export const regionalize = (value: string, locale?: string): string => {
|
|
67
|
+
if (!locale || !FS_DOMAIN.test(value)) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Only the host is rewritten; anything from the first "/" on is left alone.
|
|
72
|
+
const pathIndex = value.indexOf('/');
|
|
73
|
+
const host = pathIndex === -1 ? value : value.slice(0, pathIndex);
|
|
74
|
+
const path = pathIndex === -1 ? '' : value.slice(pathIndex);
|
|
75
|
+
|
|
76
|
+
const labels = host.split('.');
|
|
77
|
+
// FS_DOMAIN guarantees at least "fullstory.com", so this is never negative.
|
|
78
|
+
const insertAt = labels.length - 2;
|
|
79
|
+
const existing = insertAt > 0 ? labels[insertAt - 1] : undefined;
|
|
80
|
+
|
|
81
|
+
if (existing === locale) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (existing !== undefined && REGION_LABEL.test(existing)) {
|
|
86
|
+
// A different region is already present (e.g. "edge.na1.fullstory.com" on an eu1 org).
|
|
87
|
+
// Replace it rather than insert, which would yield "edge.eu1.na1.fullstory.com".
|
|
88
|
+
labels[insertAt - 1] = locale;
|
|
89
|
+
} else {
|
|
90
|
+
labels.splice(insertAt, 0, locale);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return labels.join('.') + path;
|
|
94
|
+
};
|
package/src/index.test.ts
CHANGED
|
@@ -18,6 +18,20 @@ const findBrowserInitEnv = (ns = 'FS'): Record<string, unknown> | undefined => {
|
|
|
18
18
|
return initOptions?.env;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/** Snippet's own init call (the first one), which carries the resolved orgId/host/script. */
|
|
22
|
+
const findSnippetInitEnv = (ns = 'FS'): Record<string, unknown> | undefined => {
|
|
23
|
+
const initCalls = getFsQueue(ns)?.filter((call) => call[0] === 'init') ?? [];
|
|
24
|
+
const initOptions = initCalls[0]?.[1] as { env?: Record<string, unknown> } | undefined;
|
|
25
|
+
return initOptions?.env;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** The `src` of the <script> tag the snippet injects to load fs.js. */
|
|
29
|
+
const injectedScriptSrc = (): string | undefined => {
|
|
30
|
+
const scripts = document.querySelectorAll('script[data-fs-namespace]');
|
|
31
|
+
expect(scripts).toHaveLength(1);
|
|
32
|
+
return (scripts[0] as HTMLScriptElement).src;
|
|
33
|
+
};
|
|
34
|
+
|
|
21
35
|
beforeEach(() => {
|
|
22
36
|
// Module-level init state lives in the SDK; re-import for a clean slate per test.
|
|
23
37
|
jest.resetModules();
|
|
@@ -28,6 +42,9 @@ beforeEach(() => {
|
|
|
28
42
|
}
|
|
29
43
|
});
|
|
30
44
|
delete (window as { FS?: unknown }).FS;
|
|
45
|
+
// The snippet appends to the shared jsdom document; drop prior tests' script tags so
|
|
46
|
+
// `injectedScriptSrc()` can only ever see the one injected by the test at hand.
|
|
47
|
+
document.querySelectorAll('script[data-fs-namespace]').forEach((el) => { el.remove(); });
|
|
31
48
|
});
|
|
32
49
|
|
|
33
50
|
const loadSdk = async () => {
|
|
@@ -155,6 +172,63 @@ describe('init', () => {
|
|
|
155
172
|
});
|
|
156
173
|
});
|
|
157
174
|
|
|
175
|
+
describe('region resolution', () => {
|
|
176
|
+
// fs.js only learns the org's region after it loads, so the SDK has to resolve it up
|
|
177
|
+
// front or a regional org fetches fs.js and its settings from the na1 edge (VAL-10545).
|
|
178
|
+
const euOrg = 'o-7Y9H-eu1';
|
|
179
|
+
|
|
180
|
+
it('should fetch fs.js from the org region edge', async () => {
|
|
181
|
+
const { init } = await loadSdk();
|
|
182
|
+
init({ orgId: euOrg });
|
|
183
|
+
|
|
184
|
+
expect(injectedScriptSrc()).toContain('edge.eu1.fullstory.com/s/fs.js');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should pass the regional host and script to the snippet init', async () => {
|
|
188
|
+
const { init } = await loadSdk();
|
|
189
|
+
init({ orgId: euOrg });
|
|
190
|
+
|
|
191
|
+
const env = findSnippetInitEnv();
|
|
192
|
+
expect(env?.host).toBe('eu1.fullstory.com');
|
|
193
|
+
expect(env?.script).toBe('edge.eu1.fullstory.com/s/fs.js');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should regionalize the debug script', async () => {
|
|
197
|
+
const { init } = await loadSdk();
|
|
198
|
+
init({ orgId: euOrg, debug: true });
|
|
199
|
+
|
|
200
|
+
expect(injectedScriptSrc()).toContain('edge.eu1.fullstory.com/s/fs-debug.js');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should regionalize an explicitly set Fullstory host', async () => {
|
|
204
|
+
const { init } = await loadSdk();
|
|
205
|
+
init({ orgId: euOrg, host: 'fullstory.com', appHost: 'app.fullstory.com' });
|
|
206
|
+
|
|
207
|
+
expect(findSnippetInitEnv()?.host).toBe('eu1.fullstory.com');
|
|
208
|
+
expect(findBrowserInitEnv()?.appHost).toBe('app.eu1.fullstory.com');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should leave a self-hosted proxy untouched', async () => {
|
|
212
|
+
const { init } = await loadSdk();
|
|
213
|
+
init({ orgId: euOrg, host: 'fs.example.com', script: 'fs.example.com/fs.js' });
|
|
214
|
+
|
|
215
|
+
const env = findSnippetInitEnv();
|
|
216
|
+
expect(env?.host).toBe('fs.example.com');
|
|
217
|
+
expect(env?.script).toBe('fs.example.com/fs.js');
|
|
218
|
+
expect(injectedScriptSrc()).toContain('fs.example.com/fs.js');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should not change the defaults for an org with no region', async () => {
|
|
222
|
+
const { init } = await loadSdk();
|
|
223
|
+
init({ orgId: testOrg });
|
|
224
|
+
|
|
225
|
+
const env = findSnippetInitEnv();
|
|
226
|
+
expect(env?.host).toBe('fullstory.com');
|
|
227
|
+
expect(env?.script).toBe('edge.fullstory.com/s/fs.js');
|
|
228
|
+
expect(injectedScriptSrc()).toContain('edge.fullstory.com/s/fs.js');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
158
232
|
describe('devMode', () => {
|
|
159
233
|
let consoleWarnedMessage;
|
|
160
234
|
const oldConsoleWarn = console.warn;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { initFS, FSApi } from '@fullstory/snippet';
|
|
2
|
+
import { DEFAULT_HOST, DEFAULT_SCRIPT, orgLocale, regionalize } from './hosts';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* FullStory Client SDK snippet options.
|
|
@@ -6,13 +7,17 @@ import { initFS, FSApi } from '@fullstory/snippet';
|
|
|
6
7
|
* - orgId: Reference for your [Org Id](https://help.fullstory.com/hc/en-us/articles/360047075853) listed in FullStory.
|
|
7
8
|
* - namespace: Global object name that contains the FullStory browser API methods and properties. Defaults to `FS`.
|
|
8
9
|
* - debug: Debug mode with extra browser console logging.
|
|
9
|
-
* - host: The recording server host domain. Can be set to direct recorded events to a proxy that you host. Defaults to `fullstory.com
|
|
10
|
-
* - script: FullStory script host domain. FullStory hosts the `fs.js` recording script on a CDN, but you can choose to host a copy yourself. Defaults to `edge.fullstory.com
|
|
10
|
+
* - host: The recording server host domain. Can be set to direct recorded events to a proxy that you host. Defaults to `fullstory.com`, or the region-specific equivalent (e.g. `eu1.fullstory.com`) when `orgId` carries a region suffix.
|
|
11
|
+
* - script: FullStory script host domain. FullStory hosts the `fs.js` recording script on a CDN, but you can choose to host a copy yourself. Defaults to `edge.fullstory.com/s/fs.js`, or the region-specific equivalent (e.g. `edge.eu1.fullstory.com/s/fs.js`) when `orgId` carries a region suffix.
|
|
11
12
|
* - cookieDomain: Overrides the cookie domain. By default, cookies will be valid for all subdomains of your site; if you want to limit the cookies to a specific subdomain, you can set the domain value explicitly. More information can be found [here](https://help.fullstory.com/hc/en-us/articles/360020622874-Can-the-FullStory-cookie-be-associated-with-a-specific-subdomain-).
|
|
12
13
|
* - 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).
|
|
13
14
|
* - 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).
|
|
14
15
|
* - 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`.
|
|
15
16
|
* - sessionUid (beta): Sets the session UID passed to the `FS('init', ...)` v2 operation.
|
|
17
|
+
*
|
|
18
|
+
* The region encoded in `orgId` (e.g. the `eu1` in `o-ABC123-eu1`) is applied to `host`, `script` and `appHost`
|
|
19
|
+
* whenever those resolve to a FullStory-owned domain, including values you set explicitly. Hosts FullStory does
|
|
20
|
+
* not own — a proxy or Relay domain of your own — are always used exactly as given.
|
|
16
21
|
*/
|
|
17
22
|
export interface SnippetOptions {
|
|
18
23
|
orgId: string;
|
|
@@ -121,6 +126,18 @@ const _init = (inputOptions: SnippetOptions, readyCallback?: ReadyCallback) => {
|
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
// `fs.js` resolves the org's region itself, but not until after it has loaded — the URL
|
|
130
|
+
// that fetches it has to be regionalized here instead (VAL-10545). Fullstory-owned hosts
|
|
131
|
+
// pick up the region label; customer proxies and Relay hosts are left alone.
|
|
132
|
+
const locale = orgLocale(options.orgId);
|
|
133
|
+
if (locale) {
|
|
134
|
+
options.host = regionalize(options.host || DEFAULT_HOST, locale);
|
|
135
|
+
options.script = regionalize(options.script || DEFAULT_SCRIPT, locale);
|
|
136
|
+
if (fsInitEnv.appHost) {
|
|
137
|
+
fsInitEnv.appHost = regionalize(fsInitEnv.appHost, locale);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
initFS(options);
|
|
125
142
|
|
|
126
143
|
const fs = getFullStory(_namespace);
|