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 3 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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ 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 for details
visitor: {
// Required, see below
id: '<ID_HERE>',
// Optional, see below
idMethod: '<ID_METHOD_HERE>',
},
// 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 +330,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: {
// Required, the visitor ID. This will be the yextUserId if Yext Auth is used.
id: '123919',
// Optional, the method used to generate the visitor ID.
idMethod: 'YEXT_USER',
},
```

# 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
16 changes: 15 additions & 1 deletion src/answers-search-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ class AnswersSearchBar {

this._setDefaultInitialSearch(parsedConfig.search);

this.core.init();
if (parsedConfig.visitor) {
this.setVisitor(parsedConfig.visitor);
} else {
this.core.init();
}

this._onReady = parsedConfig.onReady || function () {};

Expand Down Expand Up @@ -450,6 +454,16 @@ class AnswersSearchBar {
return value;
}
}

setVisitor (visitor) {
if (visitor.id) {
this._analyticsReporterService?.setVisitor(visitor);
this.core.storage.set(StorageKeys.VISITOR, visitor);
this.core.init();
nmanu1 marked this conversation as resolved.
Show resolved Hide resolved
} else {
console.error(`Invalid visitor. Visitor was not set because "${visitor}" does not have an id.`);
}
}
}

/**
Expand Down
16 changes: 15 additions & 1 deletion src/answers-umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,11 @@ class Answers {

this._setDefaultInitialSearch(parsedConfig.search);

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

this._onReady = parsedConfig.onReady || function () {};

Expand Down Expand Up @@ -693,6 +697,16 @@ class Answers {
return value;
}
}

setVisitor (visitor) {
if (visitor.id) {
this._analyticsReporterService?.setVisitor(visitor);
this.core.storage.set(StorageKeys.VISITOR, visitor);
this.core.init();
} else {
console.error(`Invalid visitor. Visitor was not set because "${visitor}" does not have an id.`);
}
}
}

/**
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
1 change: 1 addition & 0 deletions src/core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export default class Core {
experienceKey: this._experienceKey,
locale: this._locale,
experienceVersion: this._experienceVersion,
visitor: this.storage.get(StorageKeys.VISITOR),
nmanu1 marked this conversation as resolved.
Show resolved Hide resolved
endpoints: this._getServiceUrls(),
additionalQueryParams: {
jsLibVersion: LIB_VERSION
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;
39 changes: 39 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,42 @@ describe('ANSWERS instance integration testing', () => {
};
expect(ANSWERS._analyticsReporterService.report).toHaveBeenCalledWith(expectedEvent, expect.anything());
});

it('Visitor is set 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 is not set if missing id', async () => {
mockWindow(windowSpy, {
location: {
search: '?query=test'
}
});
await initAnswers(ANSWERS, {
visitor: { idMethod: 'test' }
});
expect(ANSWERS._analyticsReporterService.setVisitor).toHaveBeenCalledTimes(0);
expect(ANSWERS.core.storage.get(StorageKeys.VISITOR)).toBeUndefined();
});

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' } }) }));
});
});