diff --git a/.eslintrc.js b/.eslintrc.js index 33969eb..eab1588 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,14 @@ module.exports = { '@ridi/eslint-config/typescript', '@ridi/eslint-config/prettier', ], - overrides: [], + overrides: [ + { + files: ['src/**/models/*.ts'], + rules: { + camelcase: 0, + }, + }, + ], rules: { 'no-console': 0, semi: [2, 'always'], @@ -18,7 +25,8 @@ module.exports = { 'import/no-unresolved': 0, 'import/extensions': 0, 'no-void': 1, - 'no-shadow': 1, + 'no-shadow': 0, + '@typescript-eslint/no-shadow': 0, '@typescript-eslint/ban-ts-comment': 1, '@typescript-eslint/no-unsafe-assignment': 1, '@typescript-eslint/no-unsafe-call': 1, @@ -32,6 +40,8 @@ module.exports = { 'import/prefer-default-export': 0, 'func-names': 0, '@typescript-eslint/no-empty-function': 1, + 'max-classes-per-file': ['error', 10], + 'lines-around-comment': 0, }, - ignorePatterns: ['**/node_modules/**'], + ignorePatterns: ['**/node_modules/**', '**/dist/**'], }; diff --git a/package-lock.json b/package-lock.json index a6dd1cf..48033ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ridi/event-tracker", - "version": "0.10.2", + "version": "0.11.0-alpha.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -466,6 +466,12 @@ "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.39.tgz", "integrity": "sha512-AwVtVYACQg26MJz+752H6uskn53BR7eilCOHksUGkzobZNKc7O3RFTJrbD3yKAluXy6favVdynnr9btijjcakQ==" }, + "@types/gtag.js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.3.tgz", + "integrity": "sha512-iRF/4Q3G1t0OTNMjK52tpGSQPgEsYzWQL1IdVXt9BywK6MUK3ypB7LaoEQa8sW9gPXENoU1xAyCxqJhMkB2rCA==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", diff --git a/package.json b/package.json index 5afe414..096191b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ridi/event-tracker", - "version": "0.10.2", + "version": "0.11.0-alpha.7", "description": "", "main": "dist/cjs/index.js", "typings": "dist/typings/index.d.ts", @@ -12,7 +12,7 @@ "build": "npm run clean && npm run build:cjs && npm run build:umd", "build:cjs": "tsc", "build:umd": "webpack", - "deploy": "npm run build && npm publish -access public" + "deploy": "npm run build && npm publish -access public && npm install" }, "pre-commit": [ "lint" @@ -30,6 +30,7 @@ "devDependencies": { "@ridi/eslint-config": "^5.1.0", "@types/facebook-pixel": "0.0.19", + "@types/gtag.js": "0.0.3", "@types/jest": "^24.0.12", "@types/js-cookie": "^2.2.4", "@types/url-parse": "^1.4.1", diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index b14a483..43f53c3 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -1,4 +1,9 @@ -import { DeviceType, MainTrackerOptions, Tracker } from '../index'; +import { + DeviceType, + EventTrackerMethodNames, + MainTrackerOptions, + Tracker, +} from '../index'; import { BeaconTracker, GATracker, @@ -7,7 +12,7 @@ import { TagManagerTracker, TwitterTracker, } from '../trackers'; -import { BaseTracker, EventTracker } from '../trackers/base'; +import { BaseTracker } from '../trackers/base'; const ALL_TRACKERS = [ BeaconTracker, @@ -95,7 +100,7 @@ class TestableTracker extends Tracker { public mocking( trackers: Array BaseTracker>, - methodName: keyof EventTracker, + methodName: EventTrackerMethodNames, mockImpl: () => void = () => true, ) { const mockingTargetTrackers = this.getTrackerInstances(...trackers); @@ -106,7 +111,7 @@ class TestableTracker extends Tracker { public mockingAll( trackers: Array BaseTracker>, - methodNames: Array, + methodNames: Array, mockImpl: () => void = () => true, ) { return methodNames.map(m => this.mocking(trackers, m, mockImpl)); @@ -134,13 +139,12 @@ it('BeaconTracker sends PageView event with serviceProps', async () => { const sendBeaconMock = jest.fn(); // @ts-ignore - BeaconTracker.prototype.sendBeacon = sendBeaconMock; t.sendPageView(href, referrer); jest.runOnlyPendingTimers(); expect(sendBeaconMock).toHaveBeenCalledWith( - 'pageView', + 'PageView', dummpyPageMeta, { prop1: 'value1', prop2: 'value2' }, expect.any(Date), @@ -222,7 +226,6 @@ it('GATracker should send pageview event', async () => { await t.initialize(); // @ts-ignore - window.ga = jest.fn(); t.sendPageView(href, referrer); @@ -234,7 +237,7 @@ it('GATracker should send pageview event', async () => { ); }); -it('Test TwitterTracker', async () => { +it.skip('Test TwitterTracker', async () => { const t = new TestableTracker({ twitterOptions: { mainPid: 'mainPid', @@ -246,9 +249,7 @@ it('Test TwitterTracker', async () => { t.mockingAll(ALL_TRACKERS.excludes(TwitterTracker), [ 'sendPageView', - 'sendImpression', 'sendSignUp', - 'sendStartSubscription', ]); const trackPidMock = jest.fn(); @@ -257,28 +258,21 @@ it('Test TwitterTracker', async () => { const twitterTracker = t.getTrackerInstance(TwitterTracker); // @ts-ignore - twitterTracker.twttr = { conversion: {} }; - // @ts-ignore - twitterTracker.twttr.conversion.trackPid = trackPidMock; // @ts-ignore - twitterTracker.twq = twqMock; await t.initialize(); /* Need to disable flush throttling when sending event multiple times in one test cases */ // @ts-ignore - t.throttledFlush = t.flush.bind(t); t.sendPageView('href'); - t.sendImpression(); - t.sendSignUp(); - t.sendStartSubscription(); + t.sendSignUp('method'); jest.runOnlyPendingTimers(); diff --git a/src/ecommerce/constants.ts b/src/ecommerce/constants.ts new file mode 100644 index 0000000..53eee81 --- /dev/null +++ b/src/ecommerce/constants.ts @@ -0,0 +1,9 @@ +export enum Currency { + KRW = 'KRW', + USD = 'USD', +} + +export enum ServiceType { + RIDIBOOKS = 'ridibooks', + RIDISELECT = 'ridiselect', +} diff --git a/src/ecommerce/index.ts b/src/ecommerce/index.ts new file mode 100644 index 0000000..5c51ad6 --- /dev/null +++ b/src/ecommerce/index.ts @@ -0,0 +1,2 @@ +export * from './interface'; +export * from './models'; diff --git a/src/ecommerce/interface.ts b/src/ecommerce/interface.ts new file mode 100644 index 0000000..2e4bc29 --- /dev/null +++ b/src/ecommerce/interface.ts @@ -0,0 +1,28 @@ +import { PurchaseInfo } from './models/transaction'; +import { Item, Promotion } from './models'; + +export interface EcommerceTracker { + sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void; + + sendBeginCheckout(purchaseInfo: PurchaseInfo, ts?: Date): void; + + sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void; + + sendViewItem(items: Item[], ts?: Date): void; + + sendAddToPreference(items: Item[], ts?: Date): void; + + sendAddToNewBookNotification(items: Item[], ts?: Date): void; + + sendViewContent(item: Item, ts?: Date): void; + + sendViewItemList(items: Item[], ts?: Date): void; +} diff --git a/src/ecommerce/models/index.ts b/src/ecommerce/models/index.ts new file mode 100644 index 0000000..f23b885 --- /dev/null +++ b/src/ecommerce/models/index.ts @@ -0,0 +1,2 @@ +export * from './item'; +export * from './promotion'; diff --git a/src/ecommerce/models/item.ts b/src/ecommerce/models/item.ts new file mode 100644 index 0000000..5d91331 --- /dev/null +++ b/src/ecommerce/models/item.ts @@ -0,0 +1,23 @@ +import { Currency, ServiceType } from '../constants'; +import { Promotion } from './promotion'; + +export interface Item extends Partial { + readonly item_id: string; + readonly item_name: string; + readonly item_repr_id?: string; + readonly item_repr_name?: string; + readonly service_type: ServiceType; + readonly author_id?: number; + readonly author: string; + readonly item_provider_id: number; + readonly item_provider_name: string; + readonly item_category: string; + readonly item_genre?: string; + readonly coupon?: string; + readonly discount?: number; + readonly price?: number; + readonly currency?: Currency; + readonly index?: number; + readonly item_list_id?: string; + readonly item_list_name?: string; +} diff --git a/src/ecommerce/models/promotion.ts b/src/ecommerce/models/promotion.ts new file mode 100644 index 0000000..0ab86c0 --- /dev/null +++ b/src/ecommerce/models/promotion.ts @@ -0,0 +1,6 @@ +export interface Promotion { + readonly promotion_id: string; + readonly promotion_name: string; + readonly creative_name?: string; + readonly creative_slot?: number; +} diff --git a/src/ecommerce/models/transaction.ts b/src/ecommerce/models/transaction.ts new file mode 100644 index 0000000..25b075c --- /dev/null +++ b/src/ecommerce/models/transaction.ts @@ -0,0 +1,12 @@ +import { Currency } from '../constants'; +import { Item } from './item'; + +export interface PurchaseInfo { + readonly coupon_name: string; + readonly coupon: number; + readonly cash: number; + readonly point: number; + readonly currency: Currency; + readonly value: number; + readonly items: Item[]; +} diff --git a/src/index.ts b/src/index.ts index 4311b49..3e890e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,10 +4,12 @@ import URL from 'url-parse'; import { BeaconOptions, BeaconTracker, + EcommerceTracker, GAOptions, GATracker, GTagOptions, GTagTracker, + Item, KakaoOptions, KakaoTracker, PixelOptions, @@ -18,6 +20,7 @@ import { TwitterTracker, } from './trackers'; import { BaseTracker, EventTracker, PageMeta } from './trackers/base'; +import { PurchaseInfo } from './ecommerce/models/transaction'; export enum DeviceType { PC = 'pc', @@ -30,7 +33,7 @@ export type ServiceProp = Record; export interface MainTrackerOptions { debug?: boolean; development?: boolean; - userId?: string; + uId?: number; deviceType: DeviceType; serviceProps?: ServiceProp; gaOptions?: GAOptions; @@ -43,26 +46,29 @@ export interface MainTrackerOptions { twitterOptions?: TwitterOptions; } -export interface ChangeableTrackerOptions { - userId?: string; - deviceType?: DeviceType; - serviceProps?: ServiceProp; -} +type ChangeableTrackerOptions = Pick< + MainTrackerOptions, + 'uId' | 'deviceType' | 'serviceProps' +>; + +type EventParameters = + | Parameters + | Parameters; -type EventParameters = Parameters; +export type EventTrackerMethodNames = + | keyof EventTracker + | keyof EcommerceTracker; interface QueueItem { - consumerMethodName: keyof EventTracker; + consumerMethodName: EventTrackerMethodNames; eventParams: EventParameters; ts: Date; } -function pushEventToQueue(consumerMethodName?: keyof EventTracker) { - return ( - target: any, - propertyKey: keyof EventTracker, - descriptor: PropertyDescriptor, - ) => { +function pushEventToQueue( + consumerMethodName?: T, +) { + return (target: any, propertyKey: T, descriptor: PropertyDescriptor) => { consumerMethodName = consumerMethodName || propertyKey; const originalMethod = descriptor.value; @@ -163,36 +169,64 @@ export class Tracker { } } + @pushEventToQueue() + public sendEvent(name: string, data: any = {}): EventParameters { + return [name, data]; + } + @pushEventToQueue() public sendPageView(href: string, referrer?: string): EventParameters { - const pageMeta = this.getPageMeta(href, referrer); + return [this.getPageMeta(href, referrer)]; + } - return [pageMeta]; + @pushEventToQueue() + public sendScreenView( + screenName: string, + previousScreenName: string, + referrer?: string, + ): EventParameters { + return [screenName, previousScreenName, referrer]; } @pushEventToQueue() - public sendEvent(name: string, data: any = {}): EventParameters { - return [name, data]; + public sendSignUp(method: string): EventParameters { + return [method]; + } + + @pushEventToQueue() + public sendLogin(method: string): EventParameters { + return [method]; + } + + @pushEventToQueue() + public sendBeginCheckout(purchaseInfo: PurchaseInfo): EventParameters { + return [purchaseInfo]; } @pushEventToQueue() - public sendStartSubscription(): EventParameters { - return []; + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, + ): EventParameters { + return [paymentType, purchaseInfo]; } @pushEventToQueue() - public sendImpression(): EventParameters { - return []; + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, + ): EventParameters { + return [transactionId, purchaseInfo]; } @pushEventToQueue() - public sendAddPaymentInfo(): EventParameters { - return []; + public sendViewItem(items: Item[], ts?: Date): EventParameters { + return [items]; } @pushEventToQueue() - public sendSignUp(): EventParameters { - return []; + public sendViewContent(item: Item, ts?: Date): EventParameters { + return [item]; } private initializedTrackers(): BaseTracker[] { diff --git a/src/trackers/base.ts b/src/trackers/base.ts index 42a3674..4fc3e4e 100644 --- a/src/trackers/base.ts +++ b/src/trackers/base.ts @@ -1,4 +1,5 @@ import { DeviceType, MainTrackerOptions } from '../index'; +import { EcommerceTracker } from '../ecommerce'; /* eslint-disable camelcase */ export interface PageMeta { @@ -14,44 +15,25 @@ export interface PageMeta { export interface EventTracker { sendPageView(pageMeta: PageMeta, ts?: Date): void; - sendEvent(name: string, data?: Record, ts?: Date): void; - - sendSignUp(args?: Record, ts?: Date): void; - - sendStartSubscription(args?: Record, ts?: Date): void; - - sendImpression(args?: Record, ts?: Date): void; - - sendAddPaymentInfo(args?: Record, ts?: Date): void; -} - -export abstract class BaseTracker implements EventTracker { - public mainOptions: MainTrackerOptions; - - public abstract sendPageView(pageMeta: PageMeta, ts?: Date): void; - - public abstract sendEvent( - name: string, - data?: Record, + sendScreenView( + screenName: string, + previousScreenName: string, + referrer?: string, ts?: Date, ): void; - public abstract sendSignUp(args?: Record, ts?: Date): void; + sendEvent(name: string, data?: Record, ts?: Date): void; - public abstract sendImpression( - args?: Record, - ts?: Date, - ): void; + sendSignUp(method: string, ts?: Date): void; - public abstract sendStartSubscription( - args?: Record, - ts?: Date, - ): void; + sendLogin(method: string, ts?: Date): void; +} - public abstract sendAddPaymentInfo( - args?: Record, - ts?: Date, - ): void; +// https://github.com/Microsoft/TypeScript/issues/4670#issuecomment-326585615 +export interface BaseTracker extends EventTracker, EcommerceTracker {} + +export abstract class BaseTracker implements EventTracker, EcommerceTracker { + public mainOptions: MainTrackerOptions; public abstract isInitialized(): boolean; diff --git a/src/trackers/beacon.ts b/src/trackers/beacon.ts index fc4b218..5d220af 100644 --- a/src/trackers/beacon.ts +++ b/src/trackers/beacon.ts @@ -3,6 +3,9 @@ import URL from 'url-parse'; import { PVID, RUID } from '../uid'; import { UIDFactory } from '../uid/factory'; import { BaseTracker, PageMeta } from './base'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; +import { Item, Promotion } from '../ecommerce/models'; +import { convertKeyToSnakeCase } from '../utils/util'; export interface BeaconOptions { beaconSrc?: string; @@ -15,10 +18,7 @@ export class BeaconTracker extends BaseTracker { use = true, }: BeaconOptions) { super(); - this.options = { - beaconSrc, - use, - }; + this.options = { beaconSrc, use }; } private options: BeaconOptions; @@ -54,14 +54,16 @@ export class BeaconTracker extends BaseTracker { if (ts == null) { ts = new Date(); } + + data = convertKeyToSnakeCase(data); + const search = `?${URL.qs.stringify(pageMeta.query_params)}`; const log: BeaconLog = { event: eventName, - user_id: this.mainOptions.userId, - u_id: this.mainOptions.userId, + uid: this.mainOptions.uId, ruid: this.ruid.value, - pvid: this.pvid.value, + view_id: this.pvid.value, ...pageMeta, path: `${pageMeta.path}${search}`, data, @@ -81,18 +83,13 @@ export class BeaconTracker extends BaseTracker { public sendPageView(pageMeta: PageMeta, ts?: Date): void { this.pvid = new UIDFactory(PVID).create(); - this.sendBeacon( - BeaconEventName.PageView, - pageMeta, - this.mainOptions.serviceProps, - ts, - ); + this.sendBeacon('PageView', pageMeta, this.mainOptions.serviceProps, ts); this.lastPageMeta = pageMeta; } public sendEvent( name: string, - data: Record = {}, + data: Record = {}, ts?: Date, ): void { if (this.lastPageMeta === undefined) { @@ -104,29 +101,71 @@ export class BeaconTracker extends BaseTracker { this.sendBeacon(name, this.lastPageMeta, data, ts); } - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} + public sendLogin(method: string, ts?: Date): void { + this.sendEvent('Login', ts); + } - public sendImpression(args?: Record, ts?: Date): void {} + public sendSignUp(method: string, ts?: Date): void { + this.sendEvent('SignUp', { method }, ts); + } - public sendSignUp(args?: Record, ts?: Date): void {} + public sendScreenView( + screenName: string, + previousScreenName: string, + referrer?: string, + ts?: Date, + ): void { + this.sendEvent( + 'ScreenView', + { screenName, previousScreenName, referrer }, + ts, + ); + } - public sendStartSubscription( - args?: Record, + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, ts?: Date, - ): void {} -} + ): void { + this.sendEvent('AddPaymentInfo', { paymentType, ...purchaseInfo }, ts); + } + + public sendBeginCheckout(purchaseInfo: PurchaseInfo, ts?: Date): void { + this.sendEvent('BeginCheckout', { ...purchaseInfo }, ts); + } + + public sendAddToPreference(items: Item[], ts?: Date): void { + this.sendEvent('AddToPreference', { items }, ts); + } + + public sendViewItem(items: Item[], ts?: Date): void { + this.sendEvent('ViewItem', { items }, ts); + } -enum BeaconEventName { - PageView = 'pageView', + public sendViewContent(item: Item, ts?: Date): void { + this.sendEvent('ViewContent', { item }, ts); + } + + public sendAddToNewBookNotification(items: Item[], ts?: Date): void { + this.sendEvent('AddToNewBookNotification', { items }, ts); + } + + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void { + this.sendEvent('Purchase', { transactionId, ...purchaseInfo }, ts); + } } + /* eslint-disable camelcase */ interface BeaconLog extends PageMeta { - event: string; - user_id: string; - u_id: string; - ruid: string; - pvid: string; - data: Record; - ts: number; + readonly event: string; + readonly uid: number; + readonly ruid?: string; + readonly view_id: string; + readonly data: Record; + readonly ts: number; } /* eslint-enable camelcase */ diff --git a/src/trackers/ga.ts b/src/trackers/ga.ts index c740479..d70652b 100644 --- a/src/trackers/ga.ts +++ b/src/trackers/ga.ts @@ -1,5 +1,7 @@ import { loadGA } from '../utils/externalServices'; import { BaseTracker, PageMeta } from './base'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; +import { Item, Promotion } from '../ecommerce/models'; interface GAFields extends UniversalAnalytics.FieldsObject { allowAdFeatures?: boolean; @@ -11,6 +13,11 @@ export interface GAOptions { fields?: GAFields; } +/** + * @deprecated Use GTagTracker Instead + * @see GTagTracker + */ + export class GATracker extends BaseTracker { constructor(private options: GAOptions) { super(); @@ -19,11 +26,12 @@ export class GATracker extends BaseTracker { private refinePath(originalPath: string): string { const refiners: Array<(path: string) => string> = [ path => (this.options.pathPrefix ? this.options.pathPrefix + path : path), - - // Pathname in some browsers doesn't start with slash character (/) - // Ref: https://app.asana.com/0/inbox/463186034180509/765912307342230/766156873493449 - - path => (path.startsWith('/') ? path : `/${path}`), + path => + path.startsWith('/') + ? path + : `/${ + path // Ref: https://app.asana.com/0/inbox/463186034180509/765912307342230/766156873493449 // Pathname in some browsers doesn't start with slash character (/) + }`, ]; return refiners.reduce((value, refiner) => refiner(value), originalPath); @@ -73,14 +81,21 @@ export class GATracker extends BaseTracker { ga('send', fields); } - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} + public sendSignUp(method: string, ts?: Date): void {} + + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void {} - public sendImpression(args?: Record, ts?: Date): void {} + public sendViewItem(items: Item[], ts?: Date): void {} - public sendSignUp(args?: Record, ts?: Date): void {} + public sendViewItemList(items: Item[], ts?: Date): void {} - public sendStartSubscription( - args?: Record, + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, ts?: Date, ): void {} } diff --git a/src/trackers/gtag.ts b/src/trackers/gtag.ts index 865fe40..73cde3f 100644 --- a/src/trackers/gtag.ts +++ b/src/trackers/gtag.ts @@ -1,8 +1,12 @@ import { loadGTag } from '../utils/externalServices'; import { BaseTracker, PageMeta } from './base'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; +import { Item } from '../ecommerce/models'; export interface GTagOptions { trackingId: string; + autoPageView?: boolean; + defaultCurrency?: string; } declare global { @@ -14,35 +18,96 @@ declare global { export class GTagTracker extends BaseTracker { constructor(private options: GTagOptions) { super(); + options.defaultCurrency = options.defaultCurrency || 'KRW'; } - private tagCalled = false; - public async initialize(): Promise { await loadGTag(this.options.trackingId); - this.tagCalled = true; + gtag('config', this.options.trackingId, { + send_page_view: this.options.autoPageView, + // eslint-disable-next-line prettier/prettier + user_id: this.mainOptions.uId?.toString(), + }); + gtag('set', { currency: this.options.defaultCurrency }); } public isInitialized(): boolean { - return this.tagCalled; + return typeof gtag === 'function'; } - public sendPageView(pageMeta: PageMeta, ts?: Date): void {} + public sendPageView(pageMeta: PageMeta, ts?: Date): void { + gtag('event', 'page_view', { + page_location: pageMeta.href, + page_path: pageMeta.path, + page_title: pageMeta.page, + }); + } public sendEvent( name: string, data?: Record, ts?: Date, - ): void {} + ): void { + gtag('event', name, data); + } + + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void { + gtag('event', + 'add_payment_info', + { + payment_type: paymentType, + ...purchaseInfo, + coupon: purchaseInfo.coupon_name, + }); + } + + public sendSignUp(method: string, ts?: Date): void { + gtag('event', 'sign_up', { method }); + } + + public sendScreenView( + screenName: string, + previousScreenName: string, + referrer?: string, + ts?: Date, + ): void { + gtag('event', 'screen_view', { screen_name: screenName }); + } + + public sendViewItem(items: Item[], ts?: Date): void { + gtag('event', 'view_item', { items }); + } + + public sendViewItemList(items: Item[], ts?: Date): void { + gtag('event', 'view_item_list', { items }); + } - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} - public sendImpression(args?: Record, ts?: Date): void {} + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void { + gtag('event', 'purchase', { + transaction_id: transactionId, + ...purchaseInfo, + }); + } - public sendSignUp(args?: Record, ts?: Date): void {} + public sendBeginCheckout(purchaseInfo: PurchaseInfo, ts?: Date): void { + gtag('event', 'begin_checkout', { + ...purchaseInfo, + coupon: purchaseInfo.coupon_name, + }); + } public sendStartSubscription( args?: Record, ts?: Date, ): void {} + } diff --git a/src/trackers/index.ts b/src/trackers/index.ts index 192278e..93975e1 100644 --- a/src/trackers/index.ts +++ b/src/trackers/index.ts @@ -5,3 +5,4 @@ export * from './tagmanager'; export * from './gtag'; export * from './kakao'; export * from './twitter'; +export * from '../ecommerce'; diff --git a/src/trackers/kakao.ts b/src/trackers/kakao.ts index 7147d0c..693fdf4 100644 --- a/src/trackers/kakao.ts +++ b/src/trackers/kakao.ts @@ -1,5 +1,7 @@ import { loadKakao } from '../utils/externalServices'; import { BaseTracker, PageMeta } from './base'; +import { Item, Promotion } from '../ecommerce/models'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; declare let kakaoPixel: (trackingId: string) => KakaoPixel; @@ -39,22 +41,25 @@ export class KakaoTracker extends BaseTracker { this.tracker.pageView(); } - public sendSignUp(args?: Record, ts?: Date): void { + public sendSignUp(method: string, ts?: Date): void { this.tracker.completeRegistration(); } - public sendStartSubscription( - args?: Record, + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, ts?: Date, - ): void { - this.tracker.signUp(); - } + ): void {} - public sendImpression(args?: Record, ts?: Date): void { - this.tracker.viewContent(); - } + public sendViewItem(items: Item[], ts?: Date): void {} - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} + public sendViewItemList(items: Item[], ts?: Date): void {} + + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void {} public sendEvent( name: string, diff --git a/src/trackers/pixel.ts b/src/trackers/pixel.ts index 14f48d6..821f069 100644 --- a/src/trackers/pixel.ts +++ b/src/trackers/pixel.ts @@ -1,5 +1,7 @@ import { loadPixel } from '../utils/externalServices'; import { BaseTracker, PageMeta } from './base'; +import { Item, Promotion } from '../ecommerce'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; export interface PixelOptions { pixelId: string | string[]; @@ -40,20 +42,27 @@ export class PixelTracker extends BaseTracker { } } - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} - public sendEvent( name: string, data?: Record, ts?: Date, ): void {} - public sendImpression(args?: Record, ts?: Date): void {} + public sendSignUp(method: string, ts?: Date): void {} + + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void {} + + public sendViewItem(items: Item[], ts?: Date): void {} - public sendSignUp(args?: Record, ts?: Date): void {} + public sendViewItemList(items: Item[], ts?: Date): void {} - public sendStartSubscription( - args?: Record, + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, ts?: Date, ): void {} } diff --git a/src/trackers/tagmanager.ts b/src/trackers/tagmanager.ts index 4a72f3e..7e79bbb 100644 --- a/src/trackers/tagmanager.ts +++ b/src/trackers/tagmanager.ts @@ -2,6 +2,9 @@ import { MainTrackerOptions } from '..'; import { loadTagManager } from '../utils/externalServices'; import { BaseTracker, PageMeta } from './base'; +import { Item } from '../ecommerce'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; +import { convertKeyToSnakeCase } from '../utils/util'; export interface TagManagerOptions { trackingId: string; @@ -28,11 +31,18 @@ export class TagManagerTracker extends BaseTracker { return window.dataLayer; } + private pushDataLayer(data: Record): void { + this.dataLayer.push(data); + } + public setMainOptions(newOptions: MainTrackerOptions): void { - super.setMainOptions(newOptions); + if (this.mainOptions) { + this.sendEvent('OptionsChanged', newOptions); + } else { + this.pushDataLayer(newOptions); + } - this.pushDataLayer(newOptions); - this.sendEvent('Options Changed', newOptions); + super.setMainOptions(newOptions); } public async initialize(): Promise { @@ -41,37 +51,79 @@ export class TagManagerTracker extends BaseTracker { this.tagCalled = true; } + public sendEvent( + name: string, + data: Record = {}, + ts?: Date, + ): void { + data = convertKeyToSnakeCase(data); + this.pushDataLayer({ event: name, event_params: data, ts }); + } + public isInitialized(): boolean { return this.tagCalled; } public sendPageView(pageMeta: PageMeta, ts?: Date): void { - this.sendEvent('Page View', pageMeta, ts); + this.sendEvent('PageView', { ...pageMeta }, ts); } - public sendEvent( - name: string, - data: Record = {}, + public sendScreenView( + screenName: string, + previousScreenName: string, + referrer?: string, ts?: Date, ): void { - this.dataLayer.push({ - event: name, - data, - }); + this.sendEvent( + 'ScreenView', + { screenName, previousScreenName, referrer }, + ts, + ); } - private pushDataLayer(data: Record): void { - this.dataLayer.push(data); + public sendSignUp(method: string, ts?: Date): void { + this.sendEvent('SignUp', { method }, ts); } - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} + public sendLogin(method: string, ts?: Date): void { + this.sendEvent('Login', { method }, ts); + } - public sendImpression(args?: Record, ts?: Date): void {} + public sendBeginCheckout(purchaseInfo: PurchaseInfo, ts?: Date): void { + this.sendEvent('BeginCheckout', { ...purchaseInfo }, ts); + } - public sendSignUp(args?: Record, ts?: Date): void {} + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, + ts?: Date, + ): void { + this.sendEvent('AddPaymentInfo', { paymentType, ...purchaseInfo }, ts); + } + + public sendAddToPreference(items: Item[], ts?: Date): void { + this.sendEvent('AddToPreference', { items }, ts); + } - public sendStartSubscription( - args?: Record, + public sendAddToNewBookNotification(items: Item[], ts?: Date): void { + this.sendEvent('AddToNewBookNotification', { items }, ts); + } + + public sendViewItem(items: Item[], ts?: Date): void { + this.sendEvent('ViewItem', { items }, ts); + } + + public sendViewContent(item: Item, ts?: Date): void { + this.sendEvent('ViewContent', { item }, ts); + } + + public sendViewItemList(items: Item[], ts?: Date): void {} + + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, ts?: Date, - ): void {} + ): void { + this.sendEvent('Purchase', { transactionId, ...purchaseInfo }, ts); + } } diff --git a/src/trackers/twitter.ts b/src/trackers/twitter.ts index 8dbcc6e..4dd4987 100644 --- a/src/trackers/twitter.ts +++ b/src/trackers/twitter.ts @@ -3,6 +3,8 @@ import { loadTwitterUniversal, } from '../utils/externalServices'; import { BaseTracker, PageMeta } from './base'; +import { Item } from '../ecommerce'; +import { PurchaseInfo } from '../ecommerce/models/transaction'; declare let twq: any; declare let twttr: any; @@ -14,6 +16,9 @@ export interface TwitterOptions { impressionPid: string; } +/** + * @deprecated Use GTM provided tag + */ export class TwitterTracker extends BaseTracker { constructor(private options: TwitterOptions) { super(); @@ -35,39 +40,36 @@ export class TwitterTracker extends BaseTracker { return typeof this.twq === 'function' && typeof this.twttr === 'object'; } + public sendEvent( + name: string, + data?: Record, + ts?: Date, + ): void {} + public sendPageView(pageMeta: PageMeta, ts?: Date): void { this.twq('track', 'pageView'); } - public sendSignUp(args?: Record, ts?: Date): void { + public sendSignUp(method: string, ts?: Date): void { this.twttr.conversion.trackPid(this.options.booksSignUpPid, { tw_sale_amount: 0, tw_order_quantity: 0, }); } - public sendStartSubscription( - args?: Record, + public sendAddPaymentInfo( + paymentType: string, + purchaseInfo: PurchaseInfo, ts?: Date, - ): void { - this.twttr.conversion.trackPid(this.options.selectStartSubscriptionPid, { - tw_sale_amount: 0, - tw_order_quantity: 0, - }); - } + ): void {} - public sendImpression(args?: Record, ts?: Date): void { - this.twttr.conversion.trackPid(this.options.impressionPid, { - tw_sale_amount: 0, - tw_order_quantity: 0, - }); - } + public sendViewItem(items: Item[], ts?: Date): void {} - public sendAddPaymentInfo(args?: Record, ts?: Date): void {} + public sendViewItemList(items: Item[], ts?: Date): void {} - public sendEvent( - name: string, - data?: Record, + public sendPurchase( + transactionId: string, + purchaseInfo: PurchaseInfo, ts?: Date, ): void {} } diff --git a/src/utils/util.ts b/src/utils/util.ts new file mode 100644 index 0000000..5f65c60 --- /dev/null +++ b/src/utils/util.ts @@ -0,0 +1,5 @@ +import _ from 'lodash'; + +export function convertKeyToSnakeCase(obj: Record): Record { + return _.mapKeys(obj, (v, k) => _.snakeCase(k)); +}