forked from segmentio/analytics-next
-
Notifications
You must be signed in to change notification settings - Fork 5
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
Journeys In-App Plugin #6
Merged
+601
−2
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
f4853f3
In-app plugin
clabland 5e271e3
Map gist for tests. Ignore unnecessary yarn file.
clabland 9eaefcc
camelCase props
clabland 1d5734f
Remove unnecessary datacenter param
clabland 2d560d7
Remove fake-gist
nunofgs f92374c
Merge branch 'main' of github.com:customerio/cdp-analytics-js into in…
clabland b565740
Review feedback
clabland 91b5c52
Consistent naming
clabland 4cb0bb4
Update to agreed-upon delivery event name
clabland fc9fff5
Point at gist-web npm
clabland File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
packages/browser/src/plugins/in-app-plugin/__tests__/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { Analytics } from '../../../core/analytics' | ||
import { pageEnrichment } from '../../page-enrichment' | ||
import { CustomerioSettings } from '../../customerio' | ||
import { InAppPlugin, InAppPluginSettings } from '../' | ||
import Gist from 'customerio-gist-web' | ||
|
||
describe('Customer.io In-App Plugin', () => { | ||
let analytics: Analytics | ||
let gistMessageShown: Function | ||
let gistMessageAction: Function | ||
|
||
beforeEach(async () => { | ||
if(typeof analytics !== 'undefined') { | ||
analytics.reset() | ||
} | ||
|
||
jest.resetAllMocks() | ||
jest.restoreAllMocks() | ||
|
||
Gist.setup = jest.fn() | ||
Gist.clearUserToken = jest.fn() | ||
Gist.setUserToken = jest.fn() | ||
Gist.setCurrentRoute = jest.fn() | ||
Gist.events = { | ||
on: (name: string, cb: Function) => { | ||
if(name === 'messageShown') { | ||
gistMessageShown = cb | ||
} else if(name === 'messageAction') { | ||
gistMessageAction = cb | ||
} | ||
}, | ||
off: jest.fn(), | ||
} | ||
|
||
const options: CustomerioSettings = { apiKey: 'foo' } | ||
analytics = new Analytics({ writeKey: options.apiKey }) | ||
|
||
await analytics.register(InAppPlugin({ siteId: 'siteid'} as InAppPluginSettings), pageEnrichment) | ||
}) | ||
|
||
it('should setup gist with defaults', async () => { | ||
expect(Gist.setup).toBeCalledTimes(1) | ||
expect(Gist.setup).toBeCalledWith({ | ||
env: 'prod', | ||
logging: undefined, | ||
siteId: 'siteid', | ||
}) | ||
// We should clear old gist tokens on setup if we're anonymous | ||
expect(Gist.clearUserToken).toBeCalledTimes(1) | ||
}) | ||
|
||
it('should set gist route on page()', async () => { | ||
await analytics.page('testpage') | ||
expect(Gist.setCurrentRoute).toBeCalledWith('testpage') | ||
}) | ||
|
||
it('should set gist userToken on identify()', async () => { | ||
await analytics.identify('[email protected]') | ||
expect(Gist.setUserToken).toBeCalledTimes(1) | ||
expect(Gist.setUserToken).toBeCalledWith('[email protected]') | ||
}) | ||
|
||
it('should clear gist userToken on reset()', async () => { | ||
// Once during setup | ||
expect(Gist.clearUserToken).toBeCalledTimes(1) | ||
|
||
await analytics.identify('[email protected]') | ||
expect(Gist.setUserToken).toBeCalledTimes(1) | ||
expect(Gist.setUserToken).toBeCalledWith('[email protected]') | ||
|
||
await analytics.reset() | ||
|
||
// Once after reset() | ||
expect(Gist.clearUserToken).toBeCalledTimes(2) | ||
}) | ||
|
||
it('should trigger journey event for open', async () => { | ||
const spy = jest.spyOn(analytics, 'track') | ||
gistMessageShown({ | ||
properties: { | ||
gist: { | ||
campaignId: 'testcampaign', | ||
}, | ||
}, | ||
}) | ||
expect(spy).toBeCalledWith('Report Delivery Event', { | ||
deliveryId: 'testcampaign', | ||
metric: 'opened', | ||
}) | ||
}) | ||
|
||
it('should trigger journey event for non-dismiss click', async () => { | ||
const spy = jest.spyOn(analytics, 'track') | ||
gistMessageAction({ | ||
message: { | ||
properties: { | ||
messageId: 'a-test-in-app', | ||
gist: { | ||
campaignId: 'testcampaign', | ||
}, | ||
}, | ||
}, | ||
action: 'action value', | ||
name: 'action name', | ||
}) | ||
expect(spy).toBeCalledWith('Report Delivery Event', { | ||
deliveryId: 'testcampaign', | ||
metric: 'clicked', | ||
actionName: 'action name', | ||
actionValue: 'action value', | ||
}) | ||
}) | ||
|
||
it('should not trigger journey event for dismiss click', async () => { | ||
const spy = jest.spyOn(analytics, 'track') | ||
gistMessageAction({ | ||
message: { | ||
properties: { | ||
messageId: 'a-test-in-app', | ||
gist: { | ||
campaignId: 'testcampaign', | ||
}, | ||
}, | ||
}, | ||
action: 'gist://close', | ||
name: 'action name', | ||
}) | ||
expect(spy).toHaveBeenCalledTimes(0) | ||
}) | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
export enum InAppEvents { | ||
MessageOpened = 'in-app:message-opened', | ||
MessageDismissed = 'in-app:message-dismissed', | ||
MessageError = 'in-app:message-error', | ||
MessageAction = 'in-app:message-action' | ||
} | ||
|
||
export const allEvents:string[] = Object.values(InAppEvents); | ||
|
||
export enum SemanticEvents { | ||
JourneyMetric = 'Report Delivery Event', | ||
Opened = 'opened', | ||
Clicked = 'clicked', | ||
} | ||
|
||
export function newEvent(type:string, detail:any): CustomEvent { | ||
return new CustomEvent(type, { detail }) | ||
} | ||
|
||
export function gistToCIO(gistEvent:string): string { | ||
switch (gistEvent) { | ||
case 'messageShown': | ||
return InAppEvents.MessageOpened; | ||
case 'messageDismissed': | ||
return InAppEvents.MessageDismissed; | ||
case 'messageError': | ||
return InAppEvents.MessageError; | ||
case 'messageAction': | ||
return InAppEvents.MessageAction; | ||
default: | ||
return ""; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { Analytics } from '../../core/analytics' | ||
import { Context } from '../../core/context' | ||
import { Plugin } from '../../core/plugin' | ||
|
||
import { InAppEvents, SemanticEvents, newEvent, allEvents, gistToCIO } from './events' | ||
import Gist from 'customerio-gist-web' | ||
|
||
export { InAppEvents } | ||
|
||
export type InAppPluginSettings = { | ||
siteId: string | undefined | ||
events: EventListenerOrEventListenerObject | null | undefined | ||
|
||
_env: string | undefined | ||
_logging: boolean | undefined | ||
} | ||
|
||
export function InAppPlugin( | ||
settings: InAppPluginSettings, | ||
): Plugin { | ||
|
||
let _analytics: Analytics; | ||
let _gistLoaded:boolean = false; | ||
let _pluginLoaded:boolean = false; | ||
const _eventTarget:EventTarget = new EventTarget(); | ||
|
||
function attachListeners() { | ||
if(!_gistLoaded || _pluginLoaded) | ||
return; | ||
|
||
_analytics.on('reset', reset); | ||
|
||
if(settings.events) { | ||
allEvents.forEach((event) => { | ||
_eventTarget.addEventListener(event, settings?.events as EventListenerOrEventListenerObject); | ||
}); | ||
['messageShown', 'messageDismissed', 'messageError'].forEach((event) => { | ||
Gist.events.on(event, (message: any) => { | ||
_eventTarget.dispatchEvent(newEvent(gistToCIO(event), { | ||
messageId: message.messageId, | ||
deliveryId: message.properties?.gist?.campaignId, | ||
})); | ||
}); | ||
}); | ||
} | ||
|
||
Gist.events.on('messageShown', (message: any) => { | ||
const deliveryId:string = message?.properties?.gist?.campaignId; | ||
if (typeof deliveryId != 'undefined' && deliveryId != '') { | ||
_analytics.track(SemanticEvents.JourneyMetric, { | ||
'deliveryId': deliveryId, | ||
'metric': SemanticEvents.Opened, | ||
}); | ||
} | ||
}); | ||
|
||
Gist.events.on('messageAction', (params: any) => { | ||
const deliveryId:string = params?.message?.properties?.gist?.campaignId; | ||
if (params.action != 'gist://close' && typeof deliveryId != 'undefined' && deliveryId != '') { | ||
_analytics.track(SemanticEvents.JourneyMetric, { | ||
'deliveryId': deliveryId, | ||
'metric': SemanticEvents.Clicked, | ||
'actionName': params.name, | ||
'actionValue': params.action, | ||
}); | ||
} | ||
settings.events && _eventTarget.dispatchEvent(newEvent(InAppEvents.MessageAction, { | ||
messageId: params.message.messageId, | ||
deliveryId: deliveryId, | ||
action: params.action, | ||
name: params.name, | ||
actionName: params.name, | ||
actionValue: params.action, | ||
message:{ | ||
dismiss: function() { | ||
Gist.dismissMessage(params?.message?.instanceId); | ||
} | ||
} | ||
})); | ||
}); | ||
} | ||
|
||
async function page(ctx: Context): Promise<Context> { | ||
if(!_pluginLoaded) | ||
return ctx; | ||
|
||
const page:string = ctx.event?.properties?.name ?? ctx.event?.properties?.url; | ||
if(typeof page === 'string' && page.length > 0) { | ||
Gist.setCurrentRoute(page); | ||
} | ||
|
||
return ctx; | ||
} | ||
|
||
async function reset(ctx: Context): Promise<Context> { | ||
await Gist.clearUserToken(); | ||
return ctx; | ||
} | ||
|
||
async function syncUserToken(ctx: Context): Promise<Context> { | ||
if(!_gistLoaded) | ||
return ctx; | ||
|
||
const user = _analytics.user().id(); | ||
if (typeof user === 'string' && user.length > 0) { | ||
await Gist.setUserToken(user); | ||
} else { | ||
await Gist.clearUserToken(); | ||
} | ||
return ctx; | ||
} | ||
|
||
const customerio: Plugin = { | ||
name: 'Customer.io In-App Plugin', | ||
type: 'before', | ||
version: '0.0.1', | ||
isLoaded: (): boolean => _pluginLoaded, | ||
load: async (ctx: Context, instance: Analytics) => { | ||
clabland marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_analytics = instance; | ||
|
||
if(settings.siteId == null || settings.siteId == "") { | ||
_error("siteId is required. Can't initialize.") | ||
return ctx; | ||
} | ||
|
||
await Gist.setup({ | ||
siteId: settings.siteId, | ||
env: settings._env? settings._env : "prod", | ||
logging: settings._logging, | ||
}); | ||
_gistLoaded = true; | ||
|
||
await syncUserToken(ctx); | ||
attachListeners(); | ||
|
||
_pluginLoaded = true; | ||
|
||
return Promise.resolve(); | ||
}, | ||
identify: syncUserToken, | ||
page: page, | ||
unload: async () => { | ||
if(settings.events) { | ||
allEvents.forEach((event) => { | ||
_eventTarget.removeEventListener(event, settings?.events as EventListenerOrEventListenerObject); | ||
}); | ||
} | ||
}, | ||
} | ||
|
||
return customerio; | ||
} | ||
|
||
function _error(msg: string) { | ||
console.error(`[Customer.io In-App Plugin] ${msg}`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the gist sdk have the equivalent of an "unload" function? (to deregister these events)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed this offline, Gist isn't actually using event handlers, just an array of callback functions to call. So we think this is ok.