Skip to content

Commit

Permalink
Adding support for tracking AD campaigns (#501)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinejaussoin committed Mar 16, 2023
1 parent 706ff13 commit 3ffc277
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 12 deletions.
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@sentry/node": "7.36.0",
"@types/bcryptjs": "2.4.2",
"@types/connect-redis": "0.0.19",
"@types/cookie-parser": "^1.4.3",
"@types/crypto-js": "^4.1.1",
"@types/express": "4.17.14",
"@types/express-mung": "0.5.2",
Expand Down Expand Up @@ -52,6 +53,7 @@
"chalk": "5.2.0",
"chalk-template": "^0.5.0",
"connect-redis": "6.1.3",
"cookie-parser": "^1.4.6",
"copyfiles": "2.4.1",
"crypto-js": "4.1.1",
"date-fns": "2.29.3",
Expand Down
32 changes: 32 additions & 0 deletions backend/src/db/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { isSelfHostedAndLicenced } from '../../security/is-licenced.js';
import { v4 } from 'uuid';
import { hashPassword, comparePassword } from '../../encryption.js';
import { saveAndReload } from '../repositories/BaseRepository.js';
import TrackingEntity from '../entities/TrackingEntity.js';

export async function getUser(userId: string): Promise<UserEntity | null> {
return await transaction(async (manager) => {
Expand Down Expand Up @@ -192,6 +193,37 @@ export type UserRegistration = {
slackTeamId?: string;
};

export type TrackingInfo = {
campaignId: string;
creativeId: string;
device: string;
keyword: string;
gclid: string;
};

export async function associateUserWithAdWordsCampaign(
user: UserView,
tracking: Partial<TrackingInfo>
) {
return await transaction(async (manager) => {
const userRepository = manager.withRepository(UserRepository);
const userEntity = await userRepository.findOne({
where: {
id: user.id,
},
});
if (userEntity && tracking.campaignId && tracking.creativeId) {
userEntity.tracking = new TrackingEntity();
userEntity.tracking.campaignId = tracking.campaignId;
userEntity.tracking.creativeId = tracking.creativeId;
userEntity.tracking.device = tracking.device || null;
userEntity.tracking.keyword = tracking.keyword || null;
userEntity.tracking.gclid = tracking.gclid || null;
await userRepository.save(userEntity);
}
});
}

export async function registerUser(
registration: UserRegistration
): Promise<UserIdentityEntity> {
Expand Down
22 changes: 22 additions & 0 deletions backend/src/db/entities/TrackingEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Column } from 'typeorm';

export default class TrackingEntity {
@Column({ nullable: true, type: 'character varying' })
public campaignId: string | null;
@Column({ nullable: true, type: 'character varying' })
public creativeId: string | null;
@Column({ nullable: true, type: 'character varying' })
public device: string | null;
@Column({ nullable: true, type: 'character varying' })
public keyword: string | null;
@Column({ nullable: true, type: 'character varying' })
public gclid: string | null;

constructor() {
this.campaignId = null;
this.creativeId = null;
this.device = null;
this.keyword = null;
this.gclid = null;
}
}
11 changes: 5 additions & 6 deletions backend/src/db/entities/UserIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { UserIds } from '../../utils.js';
import SessionEntity from './Session.js';
import SessionTemplateEntity from './SessionTemplate.js';
import TrackingEntity from './TrackingEntity.js';

export const ALL_FIELDS: Array<keyof UserEntity> = [
'id',
Expand Down Expand Up @@ -61,12 +62,8 @@ export class UserEntity {
eager: false,
})
public sessions: SessionEntity[] | undefined;
// @OneToMany(() => UserIdentityEntity, (identity) => identity.user, {
// cascade: true,
// nullable: false,
// eager: false,
// })
// public identities: UserIdentityEntity[] | undefined;
@Column(() => TrackingEntity)
public tracking: TrackingEntity;
@Column({ nullable: true, type: 'character varying' })
public slackUserId: string | null;
@Column({ nullable: true, type: 'character varying' })
Expand All @@ -75,6 +72,7 @@ export class UserEntity {
public created: Date | undefined;
@UpdateDateColumn({ type: 'timestamp with time zone', select: false })
public updated: Date | undefined;

constructor(id: string, name: string) {
this.id = id;
this.name = name;
Expand All @@ -87,6 +85,7 @@ export class UserEntity {
this.slackTeamId = null;
this.slackUserId = null;
this.photo = null;
this.tracking = new TrackingEntity();
}

toJson(): User {
Expand Down
22 changes: 22 additions & 0 deletions backend/src/db/migrations/1678985862753-tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class tracking1678985862753 implements MigrationInterface {
name = 'tracking1678985862753'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "tracking_campaign_id" character varying`);
await queryRunner.query(`ALTER TABLE "users" ADD "tracking_creative_id" character varying`);
await queryRunner.query(`ALTER TABLE "users" ADD "tracking_device" character varying`);
await queryRunner.query(`ALTER TABLE "users" ADD "tracking_keyword" character varying`);
await queryRunner.query(`ALTER TABLE "users" ADD "tracking_gclid" character varying`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "tracking_gclid"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "tracking_keyword"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "tracking_device"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "tracking_creative_id"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "tracking_campaign_id"`);
}

}
11 changes: 11 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import {
getPasswordIdentity,
updateIdentity,
getIdentityByUsername,
associateUserWithAdWordsCampaign,
TrackingInfo,
} from './db/actions/users.js';
import { isLicenced } from './security/is-licenced.js';
import rateLimit from 'express-rate-limit';
Expand All @@ -67,6 +69,7 @@ import { QueryFailedError } from 'typeorm';
import { deleteAccount } from './db/actions/delete.js';
import { noop } from 'lodash-es';
import { createDemoSession } from './db/actions/demo.js';
import cookieParser from 'cookie-parser';

const realIpHeader = 'X-Forwarded-For';
const sessionSecret = `${config.SESSION_SECRET!}-4.11.5`; // Increment to force re-auth
Expand Down Expand Up @@ -107,6 +110,7 @@ if (config.SELF_HOSTED) {
initSentry();

const app = express();
app.use(cookieParser());

function getActualIp(req: express.Request): string {
const headerValue = req.header(realIpHeader);
Expand Down Expand Up @@ -317,6 +321,13 @@ db().then(() => {

app.get('/api/me', async (req, res) => {
const user = await getUserViewFromRequest(req);
const trackingString: string = req.cookies['retro-aw-tracking'];
if (trackingString && user) {
const tracking: Partial<TrackingInfo> = JSON.parse(trackingString);
// We don't await this because we don't want to block the response
associateUserWithAdWordsCampaign(user, tracking);
}

if (user) {
res.status(200).send(user.toJson());
} else {
Expand Down
15 changes: 15 additions & 0 deletions backend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,13 @@
dependencies:
"@types/node" "*"

"@types/cookie-parser@^1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.3.tgz#3a01df117c5705cf89a84c876b50c5a1fd427a21"
integrity sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==
dependencies:
"@types/express" "*"

"@types/cookie@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
Expand Down Expand Up @@ -1729,6 +1736,14 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==

cookie-parser@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594"
integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==
dependencies:
cookie "0.4.1"
cookie-signature "1.0.6"

[email protected]:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
Expand Down
10 changes: 8 additions & 2 deletions marketing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@next/font": "13.1.6",
"@styled-system/theme-get": "^5.1.2",
"@types/node": "18.13.0",
"@types/date-fns": "^2.6.0",
"@types/react": "18.0.28",
"@types/react-anchor-link-smooth-scroll": "^1.0.2",
"@types/react-aria-menubutton": "^6.2.9",
Expand All @@ -26,7 +26,9 @@
"@types/styled-components": "^5.1.26",
"@types/styled-system": "^5.1.16",
"@types/styled-system__theme-get": "^5.0.2",
"@types/url-parse": "^1.4.8",
"animate.css": "^4.1.1",
"date-fns": "^2.29.3",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"fs": "^0.0.1-security",
Expand Down Expand Up @@ -57,6 +59,10 @@
"sharp": "^0.31.3",
"styled-components": "^5.3.6",
"styled-system": "^5.1.5",
"typescript": "4.9.5"
"typescript": "4.9.5",
"url-parse": "^1.5.10"
},
"devDependencies": {
"@types/node": "18.15.3"
}
}
50 changes: 50 additions & 0 deletions marketing/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { addYears } from 'date-fns';
import { ResponseCookie } from 'next/dist/server/web/spec-extension/cookies';
import { NextResponse, NextRequest } from 'next/server';
import parse from 'url-parse';

// https://www.retrospected.com/?campaignid=19686942887&creative=648178043912&device=c&keyword=retro

const COOKIE_NAME = 'retro-aw-tracking';

export type TrackingInfo = {
campaignId: string;
creativeId: string;
device: string;
keyword: string;
gclid: string;
};

export function middleware(request: NextRequest) {
const response = NextResponse.next();

const url = parse(request.url, true);

if (url.query && url.query.campaignid && url.query.creative) {
const host = url.host;
const tracking: Partial<TrackingInfo> = {
campaignId: url.query.campaignid,
creativeId: url.query.creative,
device: url.query.device,
keyword: url.query.keyword,
gclid: url.query.gclid,
};

const cookie: ResponseCookie = {
name: COOKIE_NAME,
value: JSON.stringify(tracking),
sameSite: 'lax',
expires: addYears(new Date(), 1),
domain:
host.includes('localhost') || host.split('.').length < 3
? undefined
: host.split('.').slice(1).join('.'),
};

console.log('tracking', tracking);

response.cookies.set(cookie);
}

return response;
}
43 changes: 39 additions & 4 deletions marketing/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,13 @@
dependencies:
tslib "^2.4.0"

"@types/date-fns@^2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1"
integrity sha512-9DSw2ZRzV0Tmpa6PHHJbMcZn79HHus+BBBohcOaDzkK/G3zMjDUDYjJIWBFLbkh+1+/IOS0A59BpQfdr37hASg==
dependencies:
date-fns "*"

"@types/debug@^4.0.0":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
Expand Down Expand Up @@ -517,10 +524,10 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==

"@types/node@18.13.0":
version "18.13.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
"@types/node@18.15.3":
version "18.15.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==

"@types/parse5@^6.0.0":
version "6.0.3"
Expand Down Expand Up @@ -614,6 +621,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==

"@types/url-parse@^1.4.8":
version "1.4.8"
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.8.tgz#c3825047efbca1295b7f1646f38203d9145130d6"
integrity sha512-zqqcGKyNWgTLFBxmaexGUKQyWqeG7HjXj20EuQJSJWwXe54BjX0ihIo5cJB9yAQzH8dNugJ9GvkBYMjPXs/PJw==

"@typescript-eslint/parser@^5.42.0":
version "5.52.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.52.0.tgz#73c136df6c0133f1d7870de7131ccf356f5be5a4"
Expand Down Expand Up @@ -1042,6 +1054,11 @@ damerau-levenshtein@^1.0.8:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==

date-fns@*, date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==

debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
Expand Down Expand Up @@ -2942,6 +2959,11 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==

querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==

queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
Expand Down Expand Up @@ -3229,6 +3251,11 @@ remark@^14.0.2:
remark-stringify "^10.0.0"
unified "^10.0.0"

requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==

resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
Expand Down Expand Up @@ -3783,6 +3810,14 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"

url-parse@^1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"

util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Expand Down

0 comments on commit 3ffc277

Please sign in to comment.