Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add visitor support #1564

Merged
merged 7 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ function initAnswers() {
apiKey: '<API_KEY_HERE>',
// Required, the key used for your Answers experience
experienceKey: '<EXPERIENCE_KEY_HERE>',
// Optional, visitor interacting with the experience, see Visitor Configuration below
visitor: '<VISTOR_HERE>',
nmanu1 marked this conversation as resolved.
Show resolved Hide resolved
// Optional, initialize components here, invoked when the Answers component library is loaded/ready.
// If components are not added here, they can also be added when the init promise resolves
onReady: function() {},
Expand Down Expand Up @@ -323,6 +325,21 @@ function (searchParams) => {
}),
```

## Visitor Configuration

Below is a list of configuration attributes related to a visitor, used in the [base configuration](#answersinit-configuration-options) above.

The visitor object ties a user's identity to their searches and actions. The visitor can also be set or changed using the `ANSWERS.setVisitor` function.

```js
visitor: {
// Optional, the method used to generate the visitor ID.
idMethod: 'YEXT_USER',
// The visitor ID. This will be the yextUserId if Yext Auth is used.
id: '123919',
},
```

# Component Usage

The Answers Component Library exposes an easy to use interface for adding and customizing various types of UI components on your page.
Expand Down
9 changes: 9 additions & 0 deletions src/answers-umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ class Answers {
componentManager: this.components
});

if (parsedConfig.visitor) {
this.setVisitor(parsedConfig.visitor);
nmanu1 marked this conversation as resolved.
Show resolved Hide resolved
}

if (parsedConfig.onStateChange && typeof parsedConfig.onStateChange === 'function') {
parsedConfig.onStateChange(
Object.fromEntries(storage.getAll()),
Expand Down Expand Up @@ -693,6 +697,11 @@ class Answers {
return value;
}
}

setVisitor (visitor) {
this._analyticsReporterService?.setVisitor(visitor);
this.core.storage.set(StorageKeys.VISITOR, visitor);
nmanu1 marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/core/analytics/analyticsreporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export default class AnalyticsReporter {
this._globalOptions.queryId = queryId;
}

setVisitor (visitor) {
this._globalOptions.visitor = visitor;
}

/**
* Opt in or out of analytics click events
* @param {boolean} analyticsEventsEnabled
Expand Down
15 changes: 10 additions & 5 deletions src/core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ export default class Core {
locationRadius: locationRadius === 0 ? undefined : locationRadius,
context: context && JSON.parse(context),
referrerPageUrl: referrerPageUrl,
querySource: this.storage.get(StorageKeys.QUERY_SOURCE)
querySource: this.storage.get(StorageKeys.QUERY_SOURCE),
visitor: this.storage.get(StorageKeys.VISITOR)
})
.then(response => SearchDataTransformer.transformVertical(response, this._fieldFormatters, verticalKey))
.then(data => {
Expand Down Expand Up @@ -357,7 +358,8 @@ export default class Core {
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
context: context && JSON.parse(context),
referrerPageUrl: referrerPageUrl,
querySource: this.storage.get(StorageKeys.QUERY_SOURCE)
querySource: this.storage.get(StorageKeys.QUERY_SOURCE),
visitor: this.storage.get(StorageKeys.VISITOR)
})
.then(response => SearchDataTransformer.transformUniversal(response, urls, this._fieldFormatters))
.then(data => {
Expand Down Expand Up @@ -421,7 +423,8 @@ export default class Core {
return this._coreLibrary
.universalAutocomplete({
input: input,
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
visitor: this.storage.get(StorageKeys.VISITOR)
})
.then(response => AutoCompleteResponseTransformer.transformAutoCompleteResponse(response))
.then(data => {
Expand All @@ -443,7 +446,8 @@ export default class Core {
.verticalAutocomplete({
input: input,
verticalKey: verticalKey,
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
visitor: this.storage.get(StorageKeys.VISITOR)
})
.then(response => AutoCompleteResponseTransformer.transformAutoCompleteResponse(response))
.then(data => {
Expand Down Expand Up @@ -473,7 +477,8 @@ export default class Core {
verticalKey: config.verticalKey,
fields: searchParamFields,
sectioned: config.searchParameters.sectioned,
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value
sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
visitor: this.storage.get(StorageKeys.VISITOR)
})
.then(response => AutoCompleteResponseTransformer.transformFilterSearchResponse(response))
.then(data => {
Expand Down
3 changes: 2 additions & 1 deletion src/core/storage/storagekeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const StorageKeys = {
QUERY_TRIGGER: 'queryTrigger',
FACETS_LOADED: 'facets-loaded',
QUERY_SOURCE: 'query-source',
HISTORY_POP_STATE: 'history-pop-state'
HISTORY_POP_STATE: 'history-pop-state',
VISITOR: 'visitor'
};
export default StorageKeys;
26 changes: 26 additions & 0 deletions tests/answers-umd.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ANSWERS from '../src/answers-umd';
import mockWindow from './setup/mockwindow';
import initAnswers from './setup/initanswers';
import StorageKeys from '../src/core/storage/storagekeys';

jest.mock('../src/core/analytics/analyticsreporter');

Expand Down Expand Up @@ -46,4 +47,29 @@ describe('ANSWERS instance integration testing', () => {
};
expect(ANSWERS._analyticsReporterService.report).toHaveBeenCalledWith(expectedEvent, expect.anything());
});

it('Visitor is set by analytics reporter if passed to ANSWERS.init', async () => {
mockWindow(windowSpy, {
location: {
search: '?query=test'
}
});
await initAnswers(ANSWERS, {
visitor: { id: '123' }
});
expect(ANSWERS._analyticsReporterService.setVisitor).toHaveBeenCalledWith({ id: '123' });
expect(ANSWERS.core.storage.get(StorageKeys.VISITOR)).toEqual({ id: '123' });
});

it('Visitor can be changed', async () => {
mockWindow(windowSpy, {
location: {
search: '?query=test'
}
});
await initAnswers(ANSWERS, { visitor: { id: '123', idMethod: 'test' } });
ANSWERS.setVisitor({ id: '456' });
expect(ANSWERS._analyticsReporterService.setVisitor).toHaveBeenLastCalledWith({ id: '456' });
expect(ANSWERS.core.storage.get(StorageKeys.VISITOR)).toEqual({ id: '456' });
});
});
11 changes: 11 additions & 0 deletions tests/core/analytics/analyticsreporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,15 @@ describe('reporting events', () => {
expect.anything(),
expect.objectContaining({ data: expect.not.objectContaining({ queryId: '123' }) }));
});

it('Includes visitor when set', () => {
analyticsReporter.setVisitor({ id: '123' });
const expectedEvent = new AnalyticsEvent('thumbs_up');
analyticsReporter.report(expectedEvent);

expect(mockedBeacon).toBeCalledTimes(1);
expect(mockedBeacon).toBeCalledWith(
expect.anything(),
expect.objectContaining({ data: expect.objectContaining({ visitor: { id: '123' } }) }));
});
});
64 changes: 64 additions & 0 deletions tests/core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,70 @@ describe('sessionId is passed properly', () => {
});
});

describe('visitor is passed properly', () => {
it('visitor is passed in universal search', () => {
const mockCore = getMockCore();
mockCore.storage.set(StorageKeys.VISITOR, { id: '123' });
mockCore.search();
expect(mockCore._coreLibrary.universalSearch).toHaveBeenCalledWith(
expect.objectContaining({ visitor: { id: '123' } })
);
});

it('visitor is passed in vertical search', () => {
const mockCore = getMockCore();
mockCore.storage.set(StorageKeys.VISITOR, { id: '123', idMethod: 'test' });
mockCore.verticalSearch();
expect(mockCore._coreLibrary.verticalSearch).toHaveBeenCalledWith(
expect.objectContaining({ visitor: { id: '123', idMethod: 'test' } })
);
});

it('visitor is passed in universal autocomplete', () => {
const mockCore = getMockCore();
mockCore.storage.set(StorageKeys.VISITOR, { id: '123', idMethod: 'test' });
mockCore.autoCompleteUniversal();
expect(mockCore._coreLibrary.universalAutocomplete).toHaveBeenCalledWith(
expect.objectContaining({ visitor: { id: '123', idMethod: 'test' } })
);
});

it('visitor is passed in vertical autocomplete', () => {
const mockCore = getMockCore();
mockCore.storage.set(StorageKeys.VISITOR, { id: '123', idMethod: 'test' });
mockCore.autoCompleteVertical();
expect(mockCore._coreLibrary.verticalAutocomplete).toHaveBeenCalledWith(
expect.objectContaining({ visitor: { id: '123', idMethod: 'test' } })
);
});

it('visitor is passed in filter search', () => {
const mockCore = getMockCore();
mockCore.storage.set(StorageKeys.VISITOR, { id: '123', idMethod: 'test' });
mockCore.autoCompleteFilter('test', { searchParameters: { fields: [] } });
expect(mockCore._coreLibrary.filterSearch).toHaveBeenCalledWith(
expect.objectContaining({ visitor: { id: '123', idMethod: 'test' } })
);
});

it('visitor is not passed in question submission', () => {
const mockCore = getMockCore();
mockCore.storage.set(StorageKeys.VISITOR, { id: '123', idMethod: 'test' });
mockCore.submitQuestion();
expect(mockCore._coreLibrary.submitQuestion).toHaveBeenCalledWith(
expect.not.objectContaining({ visitor: { id: '123', idMethod: 'test' } })
);
});

it('undefined visitor is passed when visitor is not set', () => {
const mockCore = getMockCore();
mockCore.search();
expect(mockCore._coreLibrary.universalSearch).toHaveBeenCalledWith(
expect.objectContaining({ visitor: undefined })
);
});
});

function getMockCore () {
const core = new Core({
storage: new Storage().init()
Expand Down