Skip to content

Commit

Permalink
feat: LBA-2137 LBA-2139 tracking referer et utm (#1556)
Browse files Browse the repository at this point in the history
* feat: model new collection

* feat: setting tracking cookie

* feat: saving application source info

* feat: prise en compte utm_source utm_medium

* feat: save en service

* feat: save source sur création user

* chore: tree shaking

* feat: enregistrement hash email

* feat: avec tests et nom collection normalisé

* Update server/src/services/application.service.ts

Co-authored-by: Rémy Auricoste <[email protected]>

* feat: unused export

---------

Co-authored-by: Rémy Auricoste <[email protected]>
  • Loading branch information
alanlr and remy-auricoste authored Oct 7, 2024
1 parent a9fd623 commit 1b0d8c1
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 38 deletions.
2 changes: 2 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ fileignoreconfig:
checksum: 38c947c87cfb508fec88cbce878f89725cbb6679e833a66bdb7fca60e3685a0a
- filename: server/src/services/referrers.service.ts
checksum: 966b0ece2b18b5a6066df531524c97ba0ad38266e782a334004e8c127b41ade6
- filename: server/src/services/trafficSource.service.test.ts
checksum: 3fb0d479999b8aa8abe2ba4e56c34ee6e409636b8ade9f10b07e6ca32081b58f
- filename: server/src/services/userRecruteur.service.ts
checksum: 5fbdf21310d8dfaf259eb39a5ca1e72a53b15493cf3a59ed42c4cb5c8f3a62ab
- filename: server/static/templates/mail-bienvenue.mjml.ejs
Expand Down
12 changes: 11 additions & 1 deletion server/src/common/utils/httpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import http from "http"
import https from "https"

import axios, { AxiosRequestConfig, CreateAxiosDefaults } from "axios"
import { FastifyRequest } from "fastify"
import { compose, transformData } from "oleoduc"
import { ITrackingCookies } from "shared/models"

import { logger } from "../logger"

Expand Down Expand Up @@ -79,4 +81,12 @@ const getHttpClient = (options: CreateAxiosDefaults<any> = {}) =>
...options,
})

export { addCsvHeaders, fetchJson, fetchStream, getHttpClient }
const getSourceFromCookies = (req: FastifyRequest) =>
<ITrackingCookies>{
utm_campaign: req?.cookies?.utm_campaign || null,
referer: req?.cookies?.referer || null,
utm_medium: req?.cookies?.utm_medium || null,
utm_source: req?.cookies?.utm_source || null,
}

export { addCsvHeaders, fetchJson, fetchStream, getHttpClient, getSourceFromCookies }
25 changes: 0 additions & 25 deletions server/src/common/utils/sha512Utils.ts

This file was deleted.

8 changes: 7 additions & 1 deletion server/src/http/controllers/application.controller.v2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { zRoutes } from "shared/index"

import { getSourceFromCookies } from "@/common/utils/httpUtils"

import { getUserFromRequest } from "../../security/authenticationService"
import { sendApplicationV2 } from "../../services/application.service"
import { Server } from "../server"
Expand Down Expand Up @@ -38,7 +40,11 @@ export default function (server: Server) {
bodyLimit: 5 * 1024 ** 2, // 5MB
},
async (req, res) => {
await sendApplicationV2({ newApplication: req.body, caller: req.body.caller || undefined })
await sendApplicationV2({
newApplication: req.body,
caller: req.body.caller || undefined,
source: getSourceFromCookies(req),
})
return res.status(200).send({})
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { badRequest, forbidden, internal, notFound } from "@hapi/boom"
import { assertUnreachable, toPublicUser, zRoutes } from "shared"
import { assertUnreachable, toPublicUser, TrafficType, zRoutes } from "shared"
import { CFA, ENTREPRISE } from "shared/constants"
import { BusinessErrorCodes } from "shared/constants/errorCodes"
import { RECRUITER_STATUS } from "shared/constants/recruteur"
import { AccessStatus } from "shared/models/roleManagement.model"
import { getLastStatusEvent } from "shared/utils/getLastStatusEvent"

import { getSourceFromCookies } from "@/common/utils/httpUtils"
import { getDbCollection } from "@/common/utils/mongodbUtils"
import { startSession } from "@/common/utils/session.service"
import config from "@/config"
Expand All @@ -25,6 +26,7 @@ import {
} from "@/services/etablissement.service"
import { Organization, upsertEntrepriseData, UserAndOrganization } from "@/services/organization.service"
import { getMainRoleManagement, getPublicUserRecruteurPropsOrError } from "@/services/roleManagement.service"
import { saveUserTrafficSourceIfAny } from "@/services/trafficSource.service"
import {
autoValidateUser,
createOrganizationUser,
Expand Down Expand Up @@ -195,7 +197,7 @@ export default (server: Server) => {
switch (type) {
case ENTREPRISE: {
const siret = req.body.establishment_siret
const result = await entrepriseOnboardingWorkflow.create({ ...req.body, siret })
const result = await entrepriseOnboardingWorkflow.create({ ...req.body, siret, source: getSourceFromCookies(req) })
if ("error" in result) {
if (result.errorCode === BusinessErrorCodes.ALREADY_EXISTS) throw forbidden(result.message, result)
else throw badRequest(result.message, result)
Expand Down Expand Up @@ -232,6 +234,7 @@ export default (server: Server) => {
is_email_checked: false,
organization: { type: CFA, cfa },
})
await saveUserTrafficSourceIfAny({ user_id: userCfa._id, type: TrafficType.CFA, source: getSourceFromCookies(req) })

const slackNotification = {
subject: "RECRUTEUR",
Expand Down
2 changes: 2 additions & 0 deletions server/src/http/controllers/jobs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { badRequest, internal, notFound } from "@hapi/boom"
import { IJob, JOB_STATUS, zRoutes } from "shared"

import { getSourceFromCookies } from "@/common/utils/httpUtils"
import { getDbCollection } from "@/common/utils/mongodbUtils"
import { getUserFromRequest } from "@/security/authenticationService"
import { Appellation } from "@/services/rome.service.types"
Expand Down Expand Up @@ -102,6 +103,7 @@ export default (server: Server) => {
idcc,
siret: establishment_siret,
opco: user.organisation,
source: getSourceFromCookies(req),
},
{
isUserValidated: true,
Expand Down
5 changes: 5 additions & 0 deletions server/src/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BusinessErrorCodes } from "shared/constants/errorCodes"
import { LBA_ITEM_TYPE, LBA_ITEM_TYPE_OLD, newItemTypeToOldItemType } from "shared/constants/lbaitem"
import { RECRUITER_STATUS } from "shared/constants/recruteur"
import { prepareMessageForMail, removeUrlsFromText } from "shared/helpers/common"
import { ITrackingCookies } from "shared/models/trafficSources.model"
import { IUserWithAccount } from "shared/models/userWithAccount.model"
import { INewApplicationV2NEWCompanySiret, INewApplicationV2NEWJobId } from "shared/routes/application.routes.v2"
import { z } from "zod"
Expand All @@ -29,6 +30,7 @@ import { getOffreAvecInfoMandataire } from "./formulaire.service"
import mailer, { sanitizeForEmail } from "./mailer.service"
import { validateCaller } from "./queryValidator.service"
import { buildLbaCompanyAddress } from "./recruteurLba.service"
import { saveApplicationTrafficSourceIfAny } from "./trafficSource.service"

const MAX_MESSAGES_PAR_OFFRE_PAR_CANDIDAT = 3
const MAX_MESSAGES_PAR_SIRET_PAR_CALLER = 20
Expand Down Expand Up @@ -186,9 +188,11 @@ export const sendApplication = async ({
export const sendApplicationV2 = async ({
newApplication,
caller,
source,
}: {
newApplication: INewApplicationV2NEWCompanySiret | INewApplicationV2NEWJobId
caller?: string
source?: ITrackingCookies
}): Promise<void> => {
let lbaJob: IJobOrCompany = { type: null as any, job: null as any, recruiter: null }

Expand Down Expand Up @@ -237,6 +241,7 @@ export const sendApplicationV2 = async ({
Body: newApplication.applicant_file_content,
})
await getDbCollection("applications").insertOne(application)
await saveApplicationTrafficSourceIfAny({ application_id: application._id, applicant_email: application.applicant_email, source })
} catch (err) {
sentryCaptureException(err)
if (caller) {
Expand Down
20 changes: 19 additions & 1 deletion server/src/services/etablissement.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@ import { setTimeout } from "timers/promises"
import { badRequest, internal, isBoom } from "@hapi/boom"
import { AxiosResponse } from "axios"
import { Filter as MongoDBFilter, ObjectId } from "mongodb"
import { IAdresseV3, IBusinessError, ICfaReferentielData, IEtablissement, IGeoPoint, ILbaCompany, ILbaCompanyLegacy, IRecruiter, ZCfaReferentielData, ZPointGeometry } from "shared"
import {
IAdresseV3,
IBusinessError,
ICfaReferentielData,
IEtablissement,
IGeoPoint,
ILbaCompany,
ILbaCompanyLegacy,
IRecruiter,
ITrackingCookies,
TrafficType,
ZCfaReferentielData,
ZPointGeometry,
} from "shared"
import { CFA, ENTREPRISE, RECRUITER_STATUS } from "shared/constants"
import { EDiffusibleStatus } from "shared/constants/diffusibleStatus"
import { BusinessErrorCodes } from "shared/constants/errorCodes"
Expand Down Expand Up @@ -36,6 +49,7 @@ import mailer, { sanitizeForEmail } from "./mailer.service"
import { getOpcoBySirenFromDB, saveOpco } from "./opco.service"
import { UserAndOrganization, updateEntrepriseOpco, upsertEntrepriseData } from "./organization.service"
import { modifyPermissionToUser } from "./roleManagement.service"
import { saveUserTrafficSourceIfAny } from "./trafficSource.service"
import { autoValidateUser as authorizeUserOnEntreprise, createOrganizationUser, setUserHasToBeManuallyValidated } from "./userRecruteur.service"

/**
Expand Down Expand Up @@ -580,6 +594,7 @@ export const entrepriseOnboardingWorkflow = {
origin,
opco,
idcc,
source,
}: {
siret: string
last_name: string
Expand All @@ -589,6 +604,7 @@ export const entrepriseOnboardingWorkflow = {
origin?: string | null
opco: string
idcc?: string
source: ITrackingCookies
},
{
isUserValidated = false,
Expand Down Expand Up @@ -640,6 +656,8 @@ export const entrepriseOnboardingWorkflow = {
is_email_checked: false,
organization: { type: ENTREPRISE, entreprise },
})
await saveUserTrafficSourceIfAny({ user_id: managingUser._id, type: TrafficType.ENTREPRISE, source })

if (isUserValidated) {
await modifyPermissionToUser(
{
Expand Down
133 changes: 133 additions & 0 deletions server/src/services/trafficSource.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useMongo } from "@tests/utils/mongo.test.utils"
import { ObjectId } from "mongodb"
import { TrafficType } from "shared/models"
import { beforeEach, describe, expect, it } from "vitest"

import { getDbCollection } from "@/common/utils/mongodbUtils"

import { hashEmail, saveApplicationTrafficSourceIfAny, saveUserTrafficSourceIfAny } from "./trafficSource.service"

useMongo()

describe("Recording traffic source", () => {
beforeEach(async () => {
return async () => {
await getDbCollection("trafficsources").deleteMany({})
}
})

it("Le hash est consistant", async () => {
const result1 = hashEmail("[email protected]")
const result2 = hashEmail("[email protected]")
const result3 = hashEmail("[email protected]")

expect.soft(result1).toEqual(result2)
expect.soft(result1).not.toEqual(result3)
})

it("Un sourceTracking est enregistré si referer", async () => {
const application_id = new ObjectId()
await saveApplicationTrafficSourceIfAny({
application_id,
applicant_email: "[email protected]",
source: {
referer: "referer1",
utm_campaign: null,
utm_medium: null,
utm_source: null,
},
})

const result = await getDbCollection("trafficsources").findOne({ application_id })

expect.soft(result).toEqual(
expect.objectContaining({
referer: "referer1",
utm_campaign: null,
utm_medium: null,
utm_source: null,
applicant_email_hash: hashEmail("[email protected]"),
user_id: null,
traffic_type: TrafficType.APPLICATION,
})
)
})

it("Un sourceTracking est enregistré si utm_campaign", async () => {
const user_id_entreprise = new ObjectId()
await saveUserTrafficSourceIfAny({
user_id: user_id_entreprise,
type: TrafficType.ENTREPRISE,
source: {
referer: null,
utm_campaign: "campaign",
utm_medium: "medium",
utm_source: "source",
},
})

const user_id_cfa = new ObjectId()
await saveUserTrafficSourceIfAny({
user_id: user_id_cfa,
type: TrafficType.CFA,
source: {
referer: "referer2",
utm_campaign: "campaign",
utm_medium: "medium",
utm_source: "source",
},
})

const resultEntreprise = await getDbCollection("trafficsources").findOne({ user_id: user_id_entreprise })

expect.soft(resultEntreprise).toEqual(
expect.objectContaining({
referer: null,
utm_campaign: "campaign",
utm_medium: "medium",
utm_source: "source",
traffic_type: TrafficType.ENTREPRISE,
applicant_email_hash: null,
user_id: user_id_entreprise,
})
)

const resultCfa = await getDbCollection("trafficsources").findOne({ user_id: user_id_cfa })

expect.soft(resultCfa).toEqual(
expect.objectContaining({
referer: "referer2",
utm_campaign: "campaign",
utm_medium: "medium",
utm_source: "source",
applicant_email_hash: null,
traffic_type: TrafficType.CFA,
user_id: user_id_cfa,
})
)
})

it("Aucun sourcetracking enregistré si ni referer ni campaign", async () => {
const application_id = new ObjectId()

const countBefore = await getDbCollection("trafficsources").countDocuments({})

await saveApplicationTrafficSourceIfAny({
application_id,
applicant_email: "[email protected]",
source: {
referer: null,
utm_campaign: null,
utm_medium: null,
utm_source: null,
},
})

const countAfter = await getDbCollection("trafficsources").countDocuments({})

const result = await getDbCollection("trafficsources").findOne({ application_id })

expect.soft(countBefore).toEqual(countAfter)
expect.soft(result).toEqual(null)
})
})
Loading

0 comments on commit 1b0d8c1

Please sign in to comment.