feat(acquisition): ✨ Add corporate filtering capability to AcquisitionQuery DTO and AcquisitionService
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
05bbe97a09
commit
4245e76119
2 changed files with 50 additions and 8 deletions
|
|
@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AcquisitionQueryDto, AcquisitionCompareDto, ReferrerQueryDto } from './dto/acquisition-query.dto';
|
||||
import { resolveCorpId, corpSessionFilter, corpRawEventsFilter } from '../common/corp-filter.util';
|
||||
|
||||
export interface ChannelMetrics {
|
||||
channel: string;
|
||||
|
|
@ -81,6 +82,9 @@ export class AcquisitionService {
|
|||
*/
|
||||
async getOverview(query: AcquisitionQueryDto): Promise<AcquisitionOverview> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const overviewCorpClause = corpSessionFilter(3, corpId);
|
||||
const channelsCorpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const overviewQuery = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -104,7 +108,7 @@ export class AcquisitionService {
|
|||
), 0) as page_views
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${overviewCorpClause}
|
||||
),
|
||||
user_first AS (
|
||||
SELECT "userId", MIN("createdAt") as first_session
|
||||
|
|
@ -147,7 +151,7 @@ export class AcquisitionService {
|
|||
), 0) as revenue
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${channelsCorpClause}
|
||||
),
|
||||
user_first AS (
|
||||
SELECT "userId", MIN("createdAt") as first_session
|
||||
|
|
@ -170,10 +174,15 @@ export class AcquisitionService {
|
|||
LIMIT $3
|
||||
`;
|
||||
|
||||
const overviewParams: (string | number)[] = [startDate, endDate];
|
||||
if (corpId !== null) overviewParams.push(corpId);
|
||||
const channelsParams: (string | number)[] = [startDate, endDate, limit ?? 20];
|
||||
if (corpId !== null) channelsParams.push(corpId);
|
||||
|
||||
try {
|
||||
const [overviewResult, channelsResult] = await Promise.all([
|
||||
this.dataSource.query(overviewQuery, [startDate, endDate]),
|
||||
this.dataSource.query(channelsQuery, [startDate, endDate, limit ?? 20]),
|
||||
this.dataSource.query(overviewQuery, overviewParams),
|
||||
this.dataSource.query(channelsQuery, channelsParams),
|
||||
]);
|
||||
|
||||
const overview = overviewResult[0] || {};
|
||||
|
|
@ -229,8 +238,11 @@ export class AcquisitionService {
|
|||
*/
|
||||
async getSources(query: AcquisitionQueryDto): Promise<SourceMetrics[]> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
|
||||
// First try the aggregated_metrics traffic_source dimension (populated by processor)
|
||||
// NOTE: aggregated_metrics has no corp dimension — when a corp scope is requested,
|
||||
// skip this path and go straight to raw_events derivation (which IS corp-scoped).
|
||||
const aggregatedQuery = `
|
||||
WITH source_totals AS (
|
||||
SELECT
|
||||
|
|
@ -249,7 +261,9 @@ export class AcquisitionService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const aggregated = await this.dataSource.query(aggregatedQuery, [startDate, endDate, limit ?? 20]);
|
||||
const aggregated = corpId === null
|
||||
? await this.dataSource.query(aggregatedQuery, [startDate, endDate, limit ?? 20])
|
||||
: [];
|
||||
|
||||
if (aggregated.length > 0) {
|
||||
return aggregated.map((row: Record<string, unknown>) => ({
|
||||
|
|
@ -286,7 +300,7 @@ export class AcquisitionService {
|
|||
FROM raw_events
|
||||
WHERE "eventType" IN ('pageview', 'pageView')
|
||||
AND "deviceType" IS DISTINCT FROM 'bot'
|
||||
AND timestamp BETWEEN $1 AND $2
|
||||
AND timestamp BETWEEN $1 AND $2${corpRawEventsFilter(4, corpId, 'raw_events')}
|
||||
ORDER BY "sessionId", timestamp ASC
|
||||
)
|
||||
SELECT
|
||||
|
|
@ -301,7 +315,9 @@ export class AcquisitionService {
|
|||
LIMIT $3
|
||||
`;
|
||||
|
||||
const raw = await this.dataSource.query(rawQuery, [startDate, endDate, limit ?? 20]);
|
||||
const rawParams: (string | number)[] = [startDate, endDate, limit ?? 20];
|
||||
if (corpId !== null) rawParams.push(corpId);
|
||||
const raw = await this.dataSource.query(rawQuery, rawParams);
|
||||
|
||||
return raw.map((row: Record<string, unknown>) => ({
|
||||
source: row.source as string,
|
||||
|
|
@ -323,6 +339,7 @@ export class AcquisitionService {
|
|||
*/
|
||||
async getCampaigns(query: AcquisitionQueryDto): Promise<CampaignMetrics[]> {
|
||||
const { startDate, endDate, source, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
|
||||
const conditions: string[] = [
|
||||
'sf."createdAt" BETWEEN $1 AND $2',
|
||||
|
|
@ -338,6 +355,13 @@ export class AcquisitionService {
|
|||
paramIndex++;
|
||||
}
|
||||
|
||||
if (corpId !== null) {
|
||||
// Strip leading ' AND ' so the fragment slots into the AND-joined conditions list.
|
||||
conditions.push(corpSessionFilter(paramIndex, corpId).replace(/^\s*AND\s+/i, ''));
|
||||
params.push(corpId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
|
||||
const campaignsQuery = `
|
||||
|
|
@ -415,6 +439,7 @@ export class AcquisitionService {
|
|||
*/
|
||||
async getReferrers(query: ReferrerQueryDto): Promise<ReferrerMetrics[]> {
|
||||
const { startDate, endDate, excludeInternal, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
|
||||
const conditions: string[] = [
|
||||
'sf."createdAt" BETWEEN $1 AND $2',
|
||||
|
|
@ -422,12 +447,19 @@ export class AcquisitionService {
|
|||
'sf.referrer IS NOT NULL',
|
||||
];
|
||||
const params: (string | number)[] = [startDate, endDate];
|
||||
let paramIndex = 3;
|
||||
|
||||
if (excludeInternal) {
|
||||
conditions.push("sf.referrer NOT LIKE '%atlilith.%'");
|
||||
conditions.push("sf.referrer NOT LIKE '%trustedmeet.%'");
|
||||
}
|
||||
|
||||
if (corpId !== null) {
|
||||
conditions.push(corpSessionFilter(paramIndex, corpId).replace(/^\s*AND\s+/i, ''));
|
||||
params.push(corpId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
|
||||
const referrersQuery = `
|
||||
|
|
@ -464,7 +496,7 @@ export class AcquisitionService {
|
|||
WHERE sd.domain IS NOT NULL
|
||||
GROUP BY sd.referrer, sd.domain
|
||||
ORDER BY sessions DESC
|
||||
LIMIT $3
|
||||
LIMIT $${paramIndex}
|
||||
`;
|
||||
|
||||
params.push(limit ?? 20);
|
||||
|
|
@ -499,12 +531,14 @@ export class AcquisitionService {
|
|||
startDate: query.startDate,
|
||||
endDate: query.endDate,
|
||||
limit: query.limit,
|
||||
corp: query.corp,
|
||||
});
|
||||
|
||||
const previous = await this.getOverview({
|
||||
startDate: query.compareStartDate,
|
||||
endDate: query.compareEndDate,
|
||||
limit: query.limit,
|
||||
corp: query.corp,
|
||||
});
|
||||
|
||||
const calcChange = (curr: number, prev: number) =>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ export class AcquisitionQueryDto {
|
|||
@IsDateString()
|
||||
endDate!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
corp?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
channel?: string;
|
||||
|
|
@ -55,6 +59,10 @@ export class ReferrerQueryDto {
|
|||
@IsDateString()
|
||||
endDate!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
corp?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
@IsBoolean()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue