Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/embed/init.impl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-underscore-dangle, camelcase */
import { Logger, LogMessageType } from '../utils/logger';
import { getFsNamespace } from '../utils/fsNamespace';
import { startsWith } from '../utils/object';
import { DataLayerObserver } from '../observer';
import {
Expand Down Expand Up @@ -112,8 +113,8 @@ export function attachDloFullStoryLifecycle(win: { [key: string]: any }): void {
return;
}

const ns = win._fs_namespace;
const fs = (typeof ns === 'string' && ns) ? win[ns] : undefined;
const ns = getFsNamespace(win as any);
const fs = win[ns];
if (typeof fs !== 'function') {
return;
}
Expand Down
6 changes: 3 additions & 3 deletions src/operators/fsApi/fsApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Operator, OperatorOptions, OperatorValidator } from '../../operator';
import { getGlobal } from '../../utils/object';
import { getFsNamespace } from '../../utils/fsNamespace';

export interface FSApiOperatorOptions extends OperatorOptions {

Expand All @@ -14,10 +15,9 @@ export default abstract class FSApiOperator implements Operator {

handleData(data: any[]): any[] | null {
const thisArg: object = getGlobal();
// @ts-ignore
const fsFunction:any = thisArg[thisArg._fs_namespace]; // eslint-disable-line
const fsFunction: any = (thisArg as any)[getFsNamespace(thisArg as any)];
if (typeof fsFunction !== 'function') {
throw new Error('_fs_namespace is not a function');
throw new Error('Fullstory namespace is not a function');
}
// subclasses will determine how to prepare the data
const realData = this.prepareData(data);
Expand Down
4 changes: 2 additions & 2 deletions src/target.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parsePath, ElementKind, select } from './selector';
import { Logger, LogMessage } from './utils/logger';
import { getFsNamespace } from './utils/fsNamespace';
import { getGlobal } from './utils/object';
import DataLayerValue from './value';

Expand Down Expand Up @@ -48,8 +49,7 @@ export default class DataLayerTarget implements DataLayerValue {
constructor(public subject: Object, public property: string, public path: string,
public selector = '') {
// NB special case added to support new FS.dataLayer observation
// eslint-disable-next-line no-underscore-dangle
if (typeof subject !== 'object' && subject !== (window as any)[(window as any)._fs_namespace]) {
if (typeof subject !== 'object' && subject !== (window as any)[getFsNamespace(window)]) {
throw new Error(LogMessage.TargetSubjectObject);
}

Expand Down
28 changes: 28 additions & 0 deletions src/utils/fsNamespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable no-underscore-dangle, import/prefer-default-export */

/**
* Resolves the FullStory client namespace on `window`.
*
* Mirrors the resolution order used by `@fullstory/browser-api`:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's say fs.js instead of @fullstory/browser-api since that's an internal package and this is theoretically an open source repo.

* 1. The `data-fs-namespace` attribute on `document.currentScript` (best-effort).
* 2. The `_fs_namespace` global set on `window` by the FullStory snippet.
* 3. The default namespace `'FS'`.
*
* Note: `document.currentScript` is only meaningful while the helper module is being
* evaluated synchronously by a script tag. DLO calls this helper from deferred callbacks
* (appender log, operator handleData, lifecycle hook), so the attribute lookup is
* opportunistic and the global is the practical fallback floor — same behavior as the
* existing `(window as any)._fs_namespace` reads it replaces.
*
* @param win the global to read from; defaults to `window`
*/
export function getFsNamespace(win: any = window): string {
try {
const script = (win && win.document && win.document.currentScript) as HTMLScriptElement | null;
const attr = script && script.getAttribute && script.getAttribute('data-fs-namespace');
if (attr) return attr;
} catch (_) {
// ignore: cross-origin or detached document access can throw
}
return (win && win._fs_namespace) || 'FS';
}
4 changes: 3 additions & 1 deletion src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable no-console, max-classes-per-file */

import { getFsNamespace } from './fsNamespace';

/**
* A LogAppender simply serializes a LogEvent to a sink.
*/
Expand Down Expand Up @@ -88,7 +90,7 @@ export class FullStoryAppender implements LogAppender {

/* eslint-disable class-methods-use-this */
log(event: LogEvent): void {
const fs = (window as any)[(window as any)._fs_namespace]; // eslint-disable-line no-underscore-dangle
const fs = (window as any)[getFsNamespace(window)];

const customEventName = 'Data Layer Observer';
const customEventSource = 'dlo-log';
Expand Down
83 changes: 83 additions & 0 deletions test/fs-namespace.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable no-underscore-dangle, @typescript-eslint/no-explicit-any */
import { expect } from 'chai';
import 'mocha';

import { getFsNamespace } from '../src/utils/fsNamespace';

/**
* Builds a minimal fake `window`-like object with optional `_fs_namespace` and
* `document.currentScript` pieces so we can exercise the resolution order without
* mutating the real jsdom globals (and without coupling this spec to whatever
* other specs leave on the real `window`).
*/
function makeWin(opts: {
fsNamespace?: string;
scriptAttr?: string | null;
hasDocument?: boolean;
throwOnCurrentScript?: boolean;
} = {}): any {
const {
fsNamespace, scriptAttr, hasDocument = true, throwOnCurrentScript = false,
} = opts;

const script = scriptAttr === undefined ? null : {
getAttribute: (name: string) => (name === 'data-fs-namespace' ? scriptAttr : null),
};

const win: any = {};
if (fsNamespace !== undefined) {
win._fs_namespace = fsNamespace;
}
if (hasDocument) {
Object.defineProperty(win, 'document', {
get() {
if (throwOnCurrentScript) {
throw new Error('boom');
}
return { currentScript: script };
},
});
}
return win;
}

describe('getFsNamespace', () => {
it('returns the data-fs-namespace attribute when set on document.currentScript', () => {
const win = makeWin({ scriptAttr: 'CustomFS', fsNamespace: 'FS' });
expect(getFsNamespace(win)).to.eq('CustomFS');
});

it('prefers the script attribute over _fs_namespace when both are present', () => {
const win = makeWin({ scriptAttr: 'AttrWins', fsNamespace: 'GlobalLoses' });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duck_hunt_laugh

expect(getFsNamespace(win)).to.eq('AttrWins');
});

it('falls back to _fs_namespace when the script attribute is missing', () => {
const win = makeWin({ scriptAttr: null, fsNamespace: 'MyFS' });
expect(getFsNamespace(win)).to.eq('MyFS');
});

it('falls back to _fs_namespace when document.currentScript is null (deferred call)', () => {
const win = makeWin({ fsNamespace: 'MyFS' });
expect(getFsNamespace(win)).to.eq('MyFS');
});

it("falls back to 'FS' when neither the script attribute nor _fs_namespace are set", () => {
const win = makeWin({});
expect(getFsNamespace(win)).to.eq('FS');
});

it('treats an empty-string script attribute as not set and falls through to the global', () => {
const win = makeWin({ scriptAttr: '', fsNamespace: 'MyFS' });
expect(getFsNamespace(win)).to.eq('MyFS');
});

it('swallows errors from document access and falls back to the global', () => {
const win = makeWin({ throwOnCurrentScript: true, fsNamespace: 'MyFS' });
expect(getFsNamespace(win)).to.eq('MyFS');
});

it("returns 'FS' when the global object is missing entirely", () => {
expect(getFsNamespace(undefined as any)).to.eq('FS');
});
});