Skip to content

Commit

Permalink
LIBWEB-1233 Braze - allow deferring initialization (segmentio#829)
Browse files Browse the repository at this point in the history
  • Loading branch information
zikaari authored Oct 26, 2022
1 parent 85d4c74 commit fb57173
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Analytics, Context } from '@segment/analytics-next'
import { Subscription } from 'src/lib/browser-destinations'
import brazeDestination, { destination } from '../index'

describe('initialization', () => {
Expand Down Expand Up @@ -77,4 +78,62 @@ describe('initialization', () => {
]
`)
})

test('can defer braze initialization when deferUntilIdentified is on', async () => {
const [updateUserProfile, trackEvent] = await brazeDestination({
api_key: 'b_123',
deferUntilIdentified: true,
subscriptions: destination.presets?.map((sub) => ({ ...sub, enabled: true })) as Subscription[],
...settings
})

jest.spyOn(destination.actions.trackEvent, 'perform')
const initializeSpy = jest.spyOn(destination, 'initialize')

const analytics = new Analytics({ writeKey: '123' })

await analytics.register(updateUserProfile, trackEvent)

// Spy on the braze APIs now that braze has been loaded.
const { instance: braze } = await initializeSpy.mock.results[0].value
const openSessionSpy = jest.spyOn(braze, 'openSession')
const logCustomEventSpy = jest.spyOn(braze, 'logCustomEvent')

await analytics.track?.({
type: 'track',
event: 'UFC',
properties: {
goat: 'hasbulla'
}
})

expect(destination.actions.trackEvent.perform).toHaveBeenCalledWith(
expect.objectContaining({
instance: expect.objectContaining({
logCustomEvent: expect.any(Function)
})
}),

expect.objectContaining({
payload: { eventName: 'UFC', eventProperties: { goat: 'hasbulla' } }
})
)

expect(analytics.user().id()).toBe(null)
expect(openSessionSpy).not.toHaveBeenCalled()
expect(logCustomEventSpy).not.toHaveBeenCalled()

await analytics.identify('27413')

await analytics.track?.({
type: 'track',
event: 'FIFA',
properties: {
goat: 'deno'
}
})

expect(openSessionSpy).toHaveBeenCalled()
expect(logCustomEventSpy).toHaveBeenCalledWith('FIFA', { goat: 'deno' })
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ testSdkVersions.forEach((sdkVersion) => {
jest.spyOn(destination.actions.trackEvent, 'perform')
const initializeSpy = jest.spyOn(destination, 'initialize')

await trackEvent.load(Context.system(), {} as Analytics)
await trackEvent.load(Context.system(), new Analytics({ writeKey: '123' }))

// Spy on the braze APIs now that braze has been loaded.
const braze = await initializeSpy.mock.results[0].value
const { instance: braze } = await initializeSpy.mock.results[0].value
const logCustomEventSpy = jest.spyOn(braze, 'logCustomEvent')

await trackEvent.track?.(
Expand All @@ -49,7 +49,9 @@ testSdkVersions.forEach((sdkVersion) => {

expect(destination.actions.trackEvent.perform).toHaveBeenCalledWith(
expect.objectContaining({
logCustomEvent: expect.any(Function)
instance: expect.objectContaining({
logCustomEvent: expect.any(Function)
})
}),

expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ testSdkVersions.forEach((sdkVersion) => {
]
})

await trackPurchase.load(Context.system(), {} as Analytics)
await trackPurchase.load(Context.system(), new Analytics({ writeKey: '123' }))

// Spy on the braze APIs now that braze has been loaded.
const braze = await initializeSpy.mock.results[0].value
const { instance: braze } = await initializeSpy.mock.results[0].value
const brazeLogPurchase = jest.spyOn(braze, 'logPurchase').mockReturnValue(true)

await trackPurchase.track?.(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('updateUserProfile', () => {
]
})

await event.load(Context.system(), {} as Analytics)
await event.load(Context.system(), new Analytics({ writeKey: '123' }))
await event.identify?.(
new Context({
type: 'identify',
Expand All @@ -66,7 +66,9 @@ describe('updateUserProfile', () => {

expect(destination.actions.updateUserProfile.perform).toHaveBeenCalledWith(
expect.objectContaining({
changeUser: expect.any(Function)
instance: expect.objectContaining({
changeUser: expect.any(Function)
})
}),

expect.objectContaining({
Expand Down Expand Up @@ -115,7 +117,9 @@ describe('updateUserProfile', () => {

expect(destination.actions.updateUserProfile.perform).toHaveBeenCalledWith(
expect.objectContaining({
changeUser: expect.any(Function)
instance: expect.objectContaining({
changeUser: expect.any(Function)
})
}),

expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ import type braze from '@braze/web-sdk'
import type appboy from '@braze/web-sdk-v3'

export type BrazeType = typeof braze | typeof appboy

export type BrazeDestinationClient = {
instance: BrazeType
ready: () => boolean
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrazeType } from '../braze-types'
import { BrazeDestinationClient } from '../braze-types'
import type { ID, SegmentEvent, User } from '@segment/analytics-next'
import type { BrowserActionDefinition } from '../../../lib/browser-destinations'
import type { Settings } from '../generated-types'
Expand Down Expand Up @@ -37,7 +37,7 @@ function shouldSendToBraze(event: SegmentEvent) {
return JSON.stringify(cachedUser.traits) !== JSON.stringify(traits)
}

const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
const action: BrowserActionDefinition<Settings, BrazeDestinationClient, Payload> = {
title: 'Debounce Middleware',
description:
'When enabled, it ensures that only events where at least one changed trait value are sent to Braze, and events with duplicate traits are not sent.',
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 44 additions & 18 deletions packages/browser-destinations/src/destinations/braze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import updateUserProfile from './updateUserProfile'
import trackPurchase from './trackPurchase'
import debounce, { resetUserCache } from './debounce'
import { defaultValues, DestinationDefinition } from '@segment/actions-core'
import { BrazeType } from './braze-types'
import { BrazeDestinationClient } from './braze-types'

declare global {
interface Window {
Expand Down Expand Up @@ -49,7 +49,7 @@ const presets: DestinationDefinition['presets'] = [
}
]

export const destination: BrowserDestinationDefinition<Settings, BrazeType> = {
export const destination: BrowserDestinationDefinition<Settings, BrazeDestinationClient> = {
name: 'Braze Web Mode (Actions)',
slug: 'actions-braze-web',
mode: 'device',
Expand Down Expand Up @@ -121,6 +121,14 @@ export const destination: BrowserDestinationDefinition<Settings, BrazeType> = {
type: 'boolean',
required: false
},
deferUntilIdentified: {
description:
'If enabled, this setting delays initialization of the Braze SDK until the user has been identified. When enabled, events for anonymous users will no longer be sent to Braze.',
label: 'Only Track Known Users',
default: false,
type: 'boolean',
required: false
},
appVersion: {
description:
'Version to which user events sent to Braze will be associated with. [See more details](https://js.appboycdn.com/web-sdk/latest/doc/modules/appboy.html#initializationoptions)',
Expand Down Expand Up @@ -266,7 +274,7 @@ export const destination: BrowserDestinationDefinition<Settings, BrazeType> = {
'By default, sessions time out after 30 minutes of inactivity. Provide a value for this configuration option to override that default with a value of your own.'
}
},
initialize: async ({ settings }, dependencies) => {
initialize: async ({ settings, analytics }, dependencies) => {
try {
const {
endpoint,
Expand All @@ -277,6 +285,7 @@ export const destination: BrowserDestinationDefinition<Settings, BrazeType> = {
versionSettings,
// @ts-expect-error same as above.
subscriptions,
deferUntilIdentified,
...expectedConfig
} = settings

Expand All @@ -290,28 +299,45 @@ export const destination: BrowserDestinationDefinition<Settings, BrazeType> = {
await dependencies.loadScript(`https://js.appboycdn.com/web-sdk/${version}/braze.no-module.min.js`)
}

const brazeObject: BrazeType = version.indexOf('3.') === 0 ? window.appboy : window.braze
let initialized = false

brazeObject.initialize(api_key, {
baseUrl: window.BRAZE_BASE_URL || endpoint,
...expectedConfig
})
const client: BrazeDestinationClient = {
instance: version.indexOf('3.') === 0 ? window.appboy : window.braze,
ready: () => {
if (initialized) {
return true
}

if (brazeObject.addSdkMetadata) {
brazeObject.addSdkMetadata([brazeObject.BrazeSdkMetadata.SEGMENT])
}
if (deferUntilIdentified && typeof analytics.user().id() !== 'string') {
return false
}

client.instance.initialize(api_key, {
baseUrl: window.BRAZE_BASE_URL || endpoint,
...expectedConfig
})

if (typeof client.instance.addSdkMetadata === 'function') {
client.instance.addSdkMetadata([client.instance.BrazeSdkMetadata.SEGMENT])
}

if (automaticallyDisplayMessages) {
if ('display' in client.instance) {
client.instance.display.automaticallyShowNewInAppMessages()
} else {
client.instance.automaticallyShowInAppMessages()
}
}

client.instance.openSession()

if (automaticallyDisplayMessages) {
if ('display' in brazeObject) {
brazeObject.display.automaticallyShowNewInAppMessages()
} else {
brazeObject.automaticallyShowInAppMessages()
return (initialized = true)
}
}

brazeObject.openSession()
client.ready()

return brazeObject
return client
} catch (e) {
throw new Error(`Failed to initialize Braze ${e}`)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { BrowserActionDefinition } from '../../../lib/browser-destinations'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { BrazeType } from '../braze-types'
import { BrazeDestinationClient } from '../braze-types'

const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
const action: BrowserActionDefinition<Settings, BrazeDestinationClient, Payload> = {
title: 'Track Event',
description: 'Reports that the current user performed a custom named event.',
defaultSubscription: 'type = "track" and event != "Order Completed"',
Expand All @@ -29,7 +29,11 @@ const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
}
},
perform: (client, event) => {
client.logCustomEvent(event.payload.eventName, event.payload.eventProperties)
if (!client.ready()) {
return
}

client.instance.logCustomEvent(event.payload.eventName, event.payload.eventProperties)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { BrazeType } from '../braze-types'
import { BrazeDestinationClient } from '../braze-types'
import { omit } from '@segment/actions-core'
import type { BrowserActionDefinition } from '../../../lib/browser-destinations'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'

const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
const action: BrowserActionDefinition<Settings, BrazeDestinationClient, Payload> = {
title: 'Track Purchase',
defaultSubscription: 'type = "track" and event = "Order Completed"',
description: 'Reports that the current user made an in-app purchase.',
Expand Down Expand Up @@ -53,14 +53,18 @@ const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
}
},
perform: (client, data) => {
if (!client.ready()) {
return
}

const payload = data.payload

const reservedKeys = Object.keys(action.fields.products.properties ?? {})
const purchaseProperties = omit(payload.purchaseProperties, reservedKeys)

if (purchaseProperties?.products && Array.isArray(purchaseProperties?.products)) {
purchaseProperties?.products?.forEach((product) => {
const result = client.logPurchase(
const result = client.instance.logPurchase(
(product.product_id as string | number).toString(),
product.price,
product.currency ?? 'USD',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import * as braze from '@braze/web-sdk'
import dayjs from '../../../lib/dayjs'
import { BrazeType } from '../braze-types'
import { BrazeDestinationClient } from '../braze-types'

const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
const action: BrowserActionDefinition<Settings, BrazeDestinationClient, Payload> = {
title: 'Update User Profile',
description: 'Updates a users profile attributes in Braze',
defaultSubscription: 'type = "identify" or type = "group"',
Expand Down Expand Up @@ -149,12 +149,16 @@ const action: BrowserActionDefinition<Settings, BrazeType, Payload> = {
},

perform: (client, { payload }) => {
if (!client.ready()) {
return
}

// TODO - addAlias / addToCustomAttributeArray?
if (payload.external_id !== undefined) {
client.changeUser(payload.external_id)
client.instance.changeUser(payload.external_id)
}

const user = client.getUser()
const user = client.instance.getUser()
if (!user) return

payload.country !== undefined && user.setCountry(payload.country)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Generated file. DO NOT MODIFY IT BY HAND.

export interface Payload {
/**
* The Phone Number to send a SMS to
*/
To: string
/**
* The message body
*/
Body: string
}

0 comments on commit fb57173

Please sign in to comment.