Skip to content

Commit

Permalink
Contentstack enhancement (#2161)
Browse files Browse the repository at this point in the history
* adding field to control attribute creation

* updating contentstack

* updating test

* tests for contentstack

* registering contentstack plugin

* completing registration

* fixing tests

* adding null check
  • Loading branch information
joe-ayoub-segment authored Jul 16, 2024
1 parent dd7c41c commit a93fe13
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# @segment/analytics-browser-actions-contentstack-browser-plugins

The Contentstack Browser Plugins browser action destination for use with @segment/analytics-next.

## License

MIT License

Copyright (c) 2024 Segment

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

## Contributing

All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@segment/analytics-browser-actions-contentstack-browser-plugins",
"version": "1.0.0",
"license": "MIT",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"main": "./dist/cjs",
"module": "./dist/esm",
"scripts": {
"build": "yarn build:esm && yarn build:cjs",
"build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
"build:esm": "tsc --outDir ./dist/esm"
},
"typings": "./dist/esm",
"dependencies": {
"@segment/browser-destination-runtime": "^1.51.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Analytics, Context } from '@segment/analytics-next'
import { Subscription } from '@segment/browser-destination-runtime/types'
import contentstack, { destination } from '../index'
import { storageFallback } from '../contentstackPlugin/utils'

const example: Subscription[] = [
{
partnerAction: 'contentstackPlugin',
name: 'Contentstack Browser Plugin',
enabled: true,
subscribe: 'type = "identify"',
mapping: {
traits: { '@path': '$.traits' }
}
}
]

let ajs: Analytics

beforeEach(async () => {})

describe('contentstack', () => {
test('should populate integrations.Contentstack.createAttributes = false if no new traits detected', async () => {
ajs = new Analytics({
writeKey: 'yyuiuyiuyiuyi'
})

ajs.reset()

const storage = storageFallback // note: UniversalStorage doesn't seem to work so well in tests

storage.set('traits', JSON.stringify({ email: 'test@messer.com' }))

const [event] = await contentstack({ subscriptions: example })

jest.spyOn(destination.actions.contentstackPlugin, 'perform')
jest.spyOn(destination, 'initialize')

await event.load(Context.system(), {} as Analytics)
expect(destination.initialize).toHaveBeenCalled()

const ctx = await event.identify?.(
new Context({
type: 'identify',
userId: '123',
traits: {
email: 'test@test.com'
}
})
)
console.log(JSON.stringify(ctx, null, 2))

expect(destination.actions.contentstackPlugin.perform).toHaveBeenCalled()
expect(ctx).not.toBeUndefined()

if (!ctx || !ctx?.event || !ctx.event.integrations) {
throw new Error('integrations is undefined')
}
const integrationsObj: { createAttributes: boolean } = ctx.event.integrations['Contentstack'] as {
createAttributes: boolean
}

expect(integrationsObj.createAttributes).toEqual(false)
})

test('should populate integrations.Contentstack.createAttributes = true if new traits detected', async () => {
ajs = new Analytics({
writeKey: 'yyuiuyiuyiuyi'
})

ajs.reset()

const storage = storageFallback // note: UniversalStorage doesn't seem to work so well in tests

storage.set('traits', JSON.stringify({ email: 'test@messer.com' }))

const [event] = await contentstack({ subscriptions: example })

jest.spyOn(destination.actions.contentstackPlugin, 'perform')
jest.spyOn(destination, 'initialize')

await event.load(Context.system(), {} as Analytics)
expect(destination.initialize).toHaveBeenCalled()

const ctx = await event.identify?.(
new Context({
type: 'identify',
userId: '123',
traits: {
email: 'test@test.com',
first_name: 'Jimmy'
}
})
)
console.log(JSON.stringify(ctx, null, 2))

expect(destination.actions.contentstackPlugin.perform).toHaveBeenCalled()
expect(ctx).not.toBeUndefined()

if (!ctx || !ctx?.event || !ctx.event.integrations) {
throw new Error('integrations is undefined')
}
const integrationsObj: { createAttributes: boolean } = ctx.event.integrations['Contentstack'] as {
createAttributes: boolean
}

expect(integrationsObj.createAttributes).toEqual(true)
})
})

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Payload } from './generated-types'
import type { Settings } from '../generated-types'
import { UniversalStorage } from '@segment/analytics-next'
import { storageFallback } from './utils'

// Change from unknown to the partner SDK types
const action: BrowserActionDefinition<Settings, {}, Payload> = {
title: 'Contentstack Browser Plugin',
description:
'Enriches all Segment payloads with a value indicating if Attributes need to be created in Contentstack before they are synced.',
platform: 'web',
hidden: false,
defaultSubscription: 'type = "track" or type = "identify" or type = "page" or type = "group" or type = "alias"',
fields: {
traits: {
type: 'object',
default: { '@path': '$.traits' },
label: 'User traits',
description: 'User Profile traits to send to Contentstack',
required: true
}
},
lifecycleHook: 'enrichment',
perform: (_, { context, analytics, payload }) => {
const storage = (analytics.storage as UniversalStorage<Record<string, string>>) ?? storageFallback

const { traits } = payload

if (traits === undefined || traits === null) {
return
}

const cacheKey = 'traits'
const cachedDataString: string | null = storage.get(cacheKey)

console.log(`cachedData = ${cachedDataString} typeof cachedData = ${typeof cachedDataString}`)

let shoudCreate = false
let cacheData: object | undefined

if (cachedDataString) {
cacheData = JSON.parse(cachedDataString)

console.log(`cacheData = ${cacheData} typeof cacheData = ${typeof cacheData}`)
console.log(`cacheData tostring = ${JSON.stringify(cacheData, null, 2)}`)

const differences = Object.keys(traits).filter((element) => !Object.keys(cacheData ?? {}).includes(element))
if (differences.length > 0) {
shoudCreate = true
}
} else {
shoudCreate = true
}

console.log(`shoudCreate = ${shoudCreate}`)

if (context.event.integrations?.All !== false || context.event.integrations['Contentstack']) {
context.updateEvent('integrations.Contentstack', {})
context.updateEvent(`integrations.Contentstack.createAttributes`, shoudCreate)
}

storage.set(cacheKey, JSON.stringify({ ...cacheData, ...traits }))

console.log(`updatedCacheData = ${storage.get(cacheKey)}`)

return
}
}

export default action
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const storageFallback = {
get: (key: string) => {
const data = window.localStorage.getItem(key)
return data
},
set: (key: string, value: string) => {
return window.localStorage.setItem(key, value)
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Settings } from './generated-types'
import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
import { browserDestination } from '@segment/browser-destination-runtime/shim'
import contentstackPlugin from './contentstackPlugin'

// Switch from unknown to the partner SDK client types
export const destination: BrowserDestinationDefinition<Settings, {}> = {
name: 'Contentstack Browser Plugins',
mode: 'device',

settings: {},

initialize: async () => {
return {}
},

actions: {
contentstackPlugin
}
}

export default browserDestination(destination)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"baseUrl": "."
},
"include": ["src"],
"exclude": ["dist", "**/__tests__"]
}

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

Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,51 @@ const action: ActionDefinition<Settings, Payload> = {
label: 'User ID',
description: 'ID for the user',
required: false
},
createAttributes: {
label: 'Create Attributes',
type: 'boolean',
description: 'Inidicates if Attributes should be created in Contentstack',
required: false,
default: {'@path': '$.integrations.Contentstack.createAttributes'}
}
},
perform: async (request, { payload, settings }) => {
const personalizeAttributesData = (await fetchAllAttributes(request, settings.personalizeApiBaseUrl)).map(
(attribute: PersonalizeAttributes) => attribute?.key
)

const attributesToCreate = Object.keys(payload.traits || {}).filter(
(trait: string) => !personalizeAttributesData.includes(trait)
)

if (attributesToCreate?.length) {
const firstAttributeRes = await createCustomAttrbute(
request,
attributesToCreate[0],
settings.personalizeApiBaseUrl

const { createAttributes } = payload
if(createAttributes){
const personalizeAttributesData = (await fetchAllAttributes(request, settings.personalizeApiBaseUrl)).map(
(attribute: PersonalizeAttributes) => attribute?.key
)
if (firstAttributeRes.status === 401) return firstAttributeRes

const otherAttributes = attributesToCreate.slice(1)

await Promise.allSettled(
otherAttributes.map((trait: string) => createCustomAttrbute(request, trait, settings.personalizeApiBaseUrl))

const attributesToCreate = Object.keys(payload.traits || {}).filter(
(trait: string) => !personalizeAttributesData.includes(trait)
)

return request(`${settings.personalizeEdgeApiBaseUrl}/user-attributes`, {
method: 'patch',
json: payload.traits,
headers: {
'x-cs-eclipse-user-uid': payload.userId ?? ''
}
})
}
if (attributesToCreate?.length) {
const firstAttributeRes = await createCustomAttrbute(
request,
attributesToCreate[0],
settings.personalizeApiBaseUrl
)
if (firstAttributeRes.status === 401) return firstAttributeRes

const otherAttributes = attributesToCreate.slice(1)

await Promise.allSettled(
otherAttributes.map((trait: string) => createCustomAttrbute(request, trait, settings.personalizeApiBaseUrl))
)
}
}

return request(`${settings.personalizeEdgeApiBaseUrl}/user-attributes`, {
method: 'patch',
json: payload.traits,
headers: {
'x-cs-eclipse-user-uid': payload.userId ?? ''
}
})

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ const destination: DestinationDefinition<Settings> = {
}
}
},
presets: [
{
name: 'Contentstack Browser Plugin',
subscribe: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"',
partnerAction: 'contentstackPlugin',
mapping: {},
type: 'automatic'
}
],
actions: {
customAttributesSync
}
Expand Down
Loading

0 comments on commit a93fe13

Please sign in to comment.