Skip to content

Commit

Permalink
Adds tracking for clicking on push notifications with actions (#518)
Browse files Browse the repository at this point in the history
  • Loading branch information
pushchris authored Oct 12, 2024
1 parent 5dbecba commit e91d8a8
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
exports.up = async function(knex) {
await knex.schema.table('projects', function(table) {
table.renameColumn('link_wrap', 'link_wrap_email')
table.boolean('link_wrap_push').defaultTo(0)
})
}

exports.down = async function(knex) {
await knex.schema.table('projects', function(table) {
table.renameColumn('link_wrap_email', 'link_wrap')
table.dropColumn('link_wrap_push')
})
}
3 changes: 2 additions & 1 deletion apps/platform/src/projects/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export default class Project extends Model {
timezone!: string
text_opt_out_message?: string
text_help_message?: string
link_wrap?: boolean
link_wrap_email?: boolean
link_wrap_push?: boolean
}

export type ProjectParams = Omit<Project, ModelParams | 'deleted_at' | 'organization_id'>
Expand Down
12 changes: 10 additions & 2 deletions apps/platform/src/projects/ProjectController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ const projectCreateParams: JSONSchemaType<ProjectParams> = {
type: 'string',
nullable: true,
},
link_wrap: {
link_wrap_email: {
type: 'boolean',
nullable: true,
},
link_wrap_push: {
type: 'boolean',
nullable: true,
},
Expand Down Expand Up @@ -145,7 +149,11 @@ const projectUpdateParams: JSONSchemaType<Partial<ProjectParams>> = {
type: 'string',
nullable: true,
},
link_wrap: {
link_wrap_email: {
type: 'boolean',
nullable: true,
},
link_wrap_push: {
type: 'boolean',
nullable: true,
},
Expand Down
7 changes: 1 addition & 6 deletions apps/platform/src/providers/email/EmailChannel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import App from '../../app'
import { Variables, Wrap } from '../../render'
import { Variables } from '../../render'
import { EmailTemplate } from '../../render/Template'
import { unsubscribeEmailLink } from '../../subscriptions/SubscriptionService'
import { encodeHashid, pick } from '../../utilities'
Expand All @@ -26,11 +26,6 @@ export default class EmailChannel {
const email: Email = {
...compiled,
to: variables.user.email,
html: Wrap({
html: compiled.html,
preheader: compiled.preheader,
variables,
}), // Add link and open tracking
headers: {
'X-Campaign-Id': encodeHashid(variables.context.campaign_id),
'X-Subscription-Id': encodeHashid(variables.context.subscription_id),
Expand Down
21 changes: 17 additions & 4 deletions apps/platform/src/render/Template.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Render, { Variables } from '.'
import Render, { Variables, Wrap } from '.'
import { Webhook } from '../providers/webhook/Webhook'
import { ChannelType } from '../config/channels'
import Model, { ModelParams } from '../core/Model'
import { isValid, IsValidSchema } from '../core/validate'
import { Email, NamedEmail } from '../providers/email/Email'
import { htmlToText } from 'html-to-text'
import { paramsToEncodedLink } from './LinkService'

export default class Template extends Model {
project_id!: number
Expand Down Expand Up @@ -78,6 +79,7 @@ export class EmailTemplate extends Template {

compile(variables: Variables): CompiledEmail {
const html = Render(this.html, variables)
const preheader = this.preheader ? Render(this.preheader, variables) : undefined
const email: CompiledEmail = {
subject: Render(this.subject, variables),
from: typeof this.from === 'string'
Expand All @@ -86,15 +88,15 @@ export class EmailTemplate extends Template {
name: Render(this.from?.name ?? '', variables),
address: Render(this.from?.address ?? '', variables),
},
html,
html: Wrap({ html, preheader, variables }), // Add link and open tracking

// If the text copy has been left empty, generate from HTML
text: this.text !== undefined && this.text !== ''
? Render(this.text, variables)
: htmlToText(html),
}

if (this.preheader) email.preheader = Render(this.preheader, variables)
if (preheader) email.preheader = preheader
if (this.reply_to) email.reply_to = Render(this.reply_to, variables)
if (this.cc) email.cc = Render(this.cc, variables)
if (this.bcc) email.bcc = Render(this.bcc, variables)
Expand Down Expand Up @@ -184,16 +186,27 @@ export class PushTemplate extends Template {
}

compile(variables: Variables): CompiledPush {
const { project, user, context } = variables
const custom = Object.keys(this.custom).reduce((body, key) => {
body[key] = Render(this.custom[key], variables)
return body
}, {} as Record<string, any>)

const url = project.link_wrap_push
? paramsToEncodedLink({
userId: user.id,
campaignId: context.campaign_id,
referenceId: context.reference_id,
redirect: this.url,
path: 'c',
})
: this.url

return {
topic: this.topic,
title: Render(this.title, variables),
body: Render(this.body, variables),
custom: { ...custom, url: this.url },
custom: { ...custom, url },
}
}

Expand Down
128 changes: 128 additions & 0 deletions apps/platform/src/render/__tests__/Template.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Variables } from '..'
import Project from '../../projects/Project'
import { User } from '../../users/User'
import { EmailTemplate, PushTemplate } from '../Template'

describe('Template', () => {
describe('compile email', () => {
test('link wrap enabled', async () => {
const url = 'https://google.com'
const template = EmailTemplate.fromJson({
type: 'email',
data: {
from: 'from',
subject: 'subject',
html: `<html><body><a href="${url}">link</a></body></html>`,
},
})
const variables: Variables = {
project: Project.fromJson({
link_wrap_email: true,
}),
user: User.fromJson({ id: 1 }),
context: {
template_id: 1,
campaign_id: 1,
reference_id: '1',
subscription_id: 1,
reference_type: 'type',
},
}
const compiled = template.compile(variables)
expect(compiled).toMatchSnapshot()
})

test('link wrap disabled', async () => {
const url = 'https://google.com'
const template = EmailTemplate.fromJson({
type: 'email',
data: {
from: 'from',
subject: 'subject',
html: `<html><body><a href="${url}">link</a></body></html>`,
},
})
const variables: Variables = {
project: Project.fromJson({
link_wrap_email: false,
}),
user: User.fromJson({ id: 1 }),
context: {
template_id: 1,
campaign_id: 1,
reference_id: '1',
subscription_id: 1,
reference_type: 'type',
},
}
const compiled = template.compile(variables)
expect(compiled).toMatchSnapshot()
})
})

describe('compile push', () => {
test('link wrap disabled', async () => {
const url = 'https://google.com'
const template = PushTemplate.fromJson({
type: 'push',
data: {
title: 'title',
topic: 'topic',
body: 'body',
url,
custom: {
key: 'value',
},
},
})
const variables: Variables = {
project: Project.fromJson({
link_wrap_push: false,
}),
user: User.fromJson({ id: 1 }),
context: {
template_id: 1,
campaign_id: 1,
reference_id: '1',
subscription_id: 1,
reference_type: 'type',
},
}
const compiled = template.compile(variables)
expect(compiled).toMatchSnapshot()
expect(compiled.custom.url).toBe(url)
})

test('link wrap enabled', async () => {
const url = 'https://google.com'
const template = PushTemplate.fromJson({
type: 'push',
data: {
title: 'title',
topic: 'topic',
body: 'body',
url,
custom: {
key: 'value',
},
},
})
const variables: Variables = {
project: Project.fromJson({
link_wrap_push: true,
}),
user: User.fromJson({ id: 1 }),
context: {
template_id: 1,
campaign_id: 1,
reference_id: '1',
subscription_id: 1,
reference_type: 'type',
},
}
const compiled = template.compile(variables)
expect(compiled).toMatchSnapshot()
expect(compiled.custom.url).not.toBe(url)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Template compile email link wrap disabled 1`] = `
Object {
"from": "from",
"html": "<html><body><a href=\\"https://google.com\\">link</a><img border=\\"0\\" width=\\"1\\" height=\\"1\\" alt=\\"\\" src=\\"https://parcelvoy.com/o?u=M8LRMWZ645&c=M8LRMWZ645&s=1\\" /></body></html>",
"subject": "subject",
"text": "link [https://google.com]",
}
`;

exports[`Template compile email link wrap enabled 1`] = `
Object {
"from": "from",
"html": "<html><body><a href=\\"https://parcelvoy.com/c?u=M8LRMWZ645&c=M8LRMWZ645&s=1&r=https%253A%252F%252Fgoogle.com\\">link</a><img border=\\"0\\" width=\\"1\\" height=\\"1\\" alt=\\"\\" src=\\"https://parcelvoy.com/o?u=M8LRMWZ645&c=M8LRMWZ645&s=1\\" /></body></html>",
"subject": "subject",
"text": "link [https://google.com]",
}
`;

exports[`Template compile push link wrap disabled 1`] = `
Object {
"body": "body",
"custom": Object {
"key": "value",
"url": "https://google.com",
},
"title": "title",
"topic": "topic",
}
`;

exports[`Template compile push link wrap enabled 1`] = `
Object {
"body": "body",
"custom": Object {
"key": "value",
"url": "https://parcelvoy.com/c?u=M8LRMWZ645&c=M8LRMWZ645&s=1&r=https%253A%252F%252Fgoogle.com",
},
"title": "title",
"topic": "topic",
}
`;
2 changes: 1 addition & 1 deletion apps/platform/src/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const Wrap = ({ html, preheader, variables: { user, context, project } }:
}

// Check if link wrapping is enabled first
if (project.link_wrap) {
if (project.link_wrap_email) {
html = clickWrapHtml(html, trackingParams)
}

Expand Down
6 changes: 4 additions & 2 deletions apps/ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,10 @@
"link": "Link",
"link_desc": "Send users to another journey.",
"link_empty": "Send users to",
"link_wrapping": "Link Wrapping",
"link_wrapping_subtitle": "Enable link wrapping for all links in messages.",
"link_wrapping_email": "Email Link Wrapping",
"link_wrapping_email_subtitle": "Enable link wrapping for all links in emails.",
"link_wrapping_push": "Push Notification Link Wrapping",
"link_wrapping_push_subtitle": "Enable link wrapping for all links in push notifications.",
"list_generation_dialog_description": "List generation will happen in the background. Please reload the page to see the latest status.",
"list_name": "List Name",
"list_save": "Save List",
Expand Down
6 changes: 4 additions & 2 deletions apps/ui/public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,10 @@
"link": "Enlace",
"link_desc": "Enviar a los usuarios a otro camino.",
"link_empty": "Enviar usuarios a",
"link_wrapping": "Ajuste de enlaces",
"link_wrapping_subtitle": "Habilite el ajuste de enlaces para todos los enlaces de los mensajes.",
"link_wrapping_email": "Ajuste de enlaces email",
"link_wrapping_email_subtitle": "Habilite el ajuste de enlaces para todos los enlaces de los mensajes de email.",
"link_wrapping_push": "Ajuste de enlaces de notificación push",
"link_wrapping_push_subtitle": "Habilite el ajuste de enlaces para todos los enlaces de los mensajes de notificación push.",
"list": "Lista",
"list_generation_dialog_description": "La lista se generará en segundo plano. Vuelva a cargar la página para ver el estado más reciente.",
"list_name": "Nombre de la lista",
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ export interface Project {
timezone: string
text_opt_out_message?: string
text_help_message?: string
link_wrap: boolean
link_wrap_email: boolean
link_wrap_push: boolean
created_at: string
updated_at: string
deleted_at?: string
Expand Down
15 changes: 10 additions & 5 deletions apps/ui/src/views/project/ProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export default function ProjectForm({ project, onSave }: ProjectFormProps) {
return (
<FormWrapper<Project>
defaultValues={defaults}
onSubmit={async ({ id, name, description, locale, timezone, text_opt_out_message, text_help_message, link_wrap }) => {
onSubmit={async ({ id, name, description, locale, timezone, text_opt_out_message, text_help_message, link_wrap_email, link_wrap_push }) => {

const params = { name, description, locale, timezone, text_opt_out_message, text_help_message, link_wrap }
const params = { name, description, locale, timezone, text_opt_out_message, text_help_message, link_wrap_email, link_wrap_push }

const project = id
? await api.projects.update(id, params)
Expand Down Expand Up @@ -92,9 +92,14 @@ export default function ProjectForm({ project, onSave }: ProjectFormProps) {
subtitle={t('sms_help_message_subtitle')} />
<SwitchField
form={form}
name="link_wrap"
label={t('link_wrapping')}
subtitle={t('link_wrapping_subtitle')} />
name="link_wrap_email"
label={t('link_wrapping_email')}
subtitle={t('link_wrapping_email_subtitle')} />
<SwitchField
form={form}
name="link_wrap_push"
label={t('link_wrapping_push')}
subtitle={t('link_wrapping_push_subtitle')} />
</>
)
}
Expand Down

0 comments on commit e91d8a8

Please sign in to comment.