@grapadigital/shared-schemas 1.0.221 → 1.0.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/schemas/content.schema.d.ts +107 -1
- package/dist/schemas/content.schema.js +181 -4
- package/dist/utils/merge-metrics-breakdown.util.d.ts +12 -0
- package/dist/utils/merge-metrics-breakdown.util.js +36 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/schemas/content.schema.ts +166 -4
- package/src/utils/merge-metrics-breakdown.util.ts +51 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -58,3 +58,4 @@ __exportStar(require("./schemas/job.schema"), exports);
|
|
|
58
58
|
__exportStar(require("./schemas/snapshot.schema"), exports);
|
|
59
59
|
__exportStar(require("./schemas/lead.schema"), exports);
|
|
60
60
|
__exportStar(require("./utils/normalize-content-link.util"), exports);
|
|
61
|
+
__exportStar(require("./utils/merge-metrics-breakdown.util"), exports);
|
|
@@ -113,6 +113,22 @@ export declare class Metrics {
|
|
|
113
113
|
cpmi?: number | null;
|
|
114
114
|
cpi?: number | null;
|
|
115
115
|
cpe?: number | null;
|
|
116
|
+
/**
|
|
117
|
+
* Alcance Potencial (o antigo PO Reach): seguidores no momento do post. Em
|
|
118
|
+
* Content.metrics soma o autor e todos os coautores. Ver GD-703.
|
|
119
|
+
*/
|
|
120
|
+
potentialReach?: number | null;
|
|
121
|
+
/** Tx. de Engaj. por Alcance: engagementCount ÷ reachCount (fração). Ver GD-703. */
|
|
122
|
+
reachEngagementRate?: number | null;
|
|
123
|
+
/** Tx. de Engaj. por Seguidores: engagementCount ÷ potentialReach (fração). Ver GD-703. */
|
|
124
|
+
followersEngagementRate?: number | null;
|
|
125
|
+
/** Nome do Client.tiers pelo potentialReach do post; null sem Tiers ou fora das faixas. Ver GD-703. */
|
|
126
|
+
tier?: string | null;
|
|
127
|
+
/**
|
|
128
|
+
* followersEngagementRate ≥ meta viralReachEngagementRate do Tier (nome da
|
|
129
|
+
* meta herdado da GD-687); null sem Tier ou sem a meta. Ver GD-703.
|
|
130
|
+
*/
|
|
131
|
+
isViral?: boolean | null;
|
|
116
132
|
}
|
|
117
133
|
export declare const MetricsSchema: MongooseSchema<Metrics, import("mongoose").Model<Metrics, any, any, any, import("mongoose").Document<unknown, any, Metrics, any, {}> & Metrics & {
|
|
118
134
|
_id: Types.ObjectId;
|
|
@@ -123,6 +139,85 @@ export declare const MetricsSchema: MongooseSchema<Metrics, import("mongoose").M
|
|
|
123
139
|
} & {
|
|
124
140
|
__v: number;
|
|
125
141
|
}>;
|
|
142
|
+
/** Coautor do post (Colab), com os seguidores dele no momento do post. Ver GD-703. */
|
|
143
|
+
export declare class ContentCollab {
|
|
144
|
+
profile: Types.ObjectId | Profile;
|
|
145
|
+
followersAtPost?: number | null;
|
|
146
|
+
}
|
|
147
|
+
export declare const ContentCollabSchema: MongooseSchema<ContentCollab, import("mongoose").Model<ContentCollab, any, any, any, import("mongoose").Document<unknown, any, ContentCollab, any, {}> & ContentCollab & {
|
|
148
|
+
_id: Types.ObjectId;
|
|
149
|
+
} & {
|
|
150
|
+
__v: number;
|
|
151
|
+
}, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentCollab, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentCollab>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentCollab> & {
|
|
152
|
+
_id: Types.ObjectId;
|
|
153
|
+
} & {
|
|
154
|
+
__v: number;
|
|
155
|
+
}>;
|
|
156
|
+
/** Chaves brutas de Metrics que têm parte Orgânica e Impulsionada. Ver GD-707. */
|
|
157
|
+
export declare const CONTENT_BASE_METRIC_KEYS: readonly ["impressionCount", "reachCount", "likeCount", "commentCount", "savedCount", "sharesCount", "storyCount", "tapBackCount", "clickCount", "replyCount", "tapForwardCount", "swipeForwardCount", "exitCount", "favoriteCount", "dislikeCount", "emv"];
|
|
158
|
+
export type ContentBaseMetricKey = (typeof CONTENT_BASE_METRIC_KEYS)[number];
|
|
159
|
+
export declare class ContentBaseMetrics {
|
|
160
|
+
impressionCount?: number | null;
|
|
161
|
+
reachCount?: number | null;
|
|
162
|
+
likeCount?: number | null;
|
|
163
|
+
commentCount?: number | null;
|
|
164
|
+
savedCount?: number | null;
|
|
165
|
+
sharesCount?: number | null;
|
|
166
|
+
storyCount?: number | null;
|
|
167
|
+
tapBackCount?: number | null;
|
|
168
|
+
clickCount?: number | null;
|
|
169
|
+
replyCount?: number | null;
|
|
170
|
+
tapForwardCount?: number | null;
|
|
171
|
+
swipeForwardCount?: number | null;
|
|
172
|
+
exitCount?: number | null;
|
|
173
|
+
favoriteCount?: number | null;
|
|
174
|
+
dislikeCount?: number | null;
|
|
175
|
+
emv?: number | null;
|
|
176
|
+
}
|
|
177
|
+
export declare const ContentBaseMetricsSchema: MongooseSchema<ContentBaseMetrics, import("mongoose").Model<ContentBaseMetrics, any, any, any, import("mongoose").Document<unknown, any, ContentBaseMetrics, any, {}> & ContentBaseMetrics & {
|
|
178
|
+
_id: Types.ObjectId;
|
|
179
|
+
} & {
|
|
180
|
+
__v: number;
|
|
181
|
+
}, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentBaseMetrics, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentBaseMetrics>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentBaseMetrics> & {
|
|
182
|
+
_id: Types.ObjectId;
|
|
183
|
+
} & {
|
|
184
|
+
__v: number;
|
|
185
|
+
}>;
|
|
186
|
+
/** Divisão Orgânico e Impulsionado; o total em Content.metrics é sempre a soma. Ver GD-707. */
|
|
187
|
+
export declare class ContentMetricsBreakdown {
|
|
188
|
+
organic: ContentBaseMetrics;
|
|
189
|
+
boosted: ContentBaseMetrics;
|
|
190
|
+
}
|
|
191
|
+
export declare const ContentMetricsBreakdownSchema: MongooseSchema<ContentMetricsBreakdown, import("mongoose").Model<ContentMetricsBreakdown, any, any, any, import("mongoose").Document<unknown, any, ContentMetricsBreakdown, any, {}> & ContentMetricsBreakdown & {
|
|
192
|
+
_id: Types.ObjectId;
|
|
193
|
+
} & {
|
|
194
|
+
__v: number;
|
|
195
|
+
}, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentMetricsBreakdown, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentMetricsBreakdown>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentMetricsBreakdown> & {
|
|
196
|
+
_id: Types.ObjectId;
|
|
197
|
+
} & {
|
|
198
|
+
__v: number;
|
|
199
|
+
}>;
|
|
200
|
+
/**
|
|
201
|
+
* Dados do vídeo para o VCR. Views não ficam aqui: impressões e views são
|
|
202
|
+
* sempre Content.metrics.impressionCount. Ver GD-707.
|
|
203
|
+
*/
|
|
204
|
+
export declare class ContentVideo {
|
|
205
|
+
/** Duração em segundos. */
|
|
206
|
+
duration?: number | null;
|
|
207
|
+
/** Tempo total assistido, somando todas as visualizações, em segundos. */
|
|
208
|
+
watchTime?: number | null;
|
|
209
|
+
/** (watchTime ÷ duration) ÷ impressionCount, sem teto. */
|
|
210
|
+
vcr?: number | null;
|
|
211
|
+
}
|
|
212
|
+
export declare const ContentVideoSchema: MongooseSchema<ContentVideo, import("mongoose").Model<ContentVideo, any, any, any, import("mongoose").Document<unknown, any, ContentVideo, any, {}> & ContentVideo & {
|
|
213
|
+
_id: Types.ObjectId;
|
|
214
|
+
} & {
|
|
215
|
+
__v: number;
|
|
216
|
+
}, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentVideo, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentVideo>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentVideo> & {
|
|
217
|
+
_id: Types.ObjectId;
|
|
218
|
+
} & {
|
|
219
|
+
__v: number;
|
|
220
|
+
}>;
|
|
126
221
|
export declare class ContentMetricsHistoryEntry {
|
|
127
222
|
metrics: Metrics;
|
|
128
223
|
recordedAt: Date;
|
|
@@ -195,8 +290,17 @@ export declare class Content {
|
|
|
195
290
|
isCollab: boolean;
|
|
196
291
|
isBoosted: boolean;
|
|
197
292
|
hasAffiliateLink: boolean;
|
|
198
|
-
collabs
|
|
293
|
+
/** Coautores do post; isCollab = collabs.length > 0. Ver GD-703. */
|
|
294
|
+
collabs?: ContentCollab[];
|
|
295
|
+
/** Métricas do post (sem métrica por pessoa): derivadas com o potentialReach somado. Ver GD-703. */
|
|
199
296
|
metrics: Metrics;
|
|
297
|
+
metricsBreakdown?: ContentMetricsBreakdown | null;
|
|
298
|
+
/**
|
|
299
|
+
* Caminhos das partes digitadas à mão (ex.: organic.reachCount,
|
|
300
|
+
* video.watchTime): a captura não sobrescreve. Ver GD-707.
|
|
301
|
+
*/
|
|
302
|
+
manualMetrics?: string[];
|
|
303
|
+
video?: ContentVideo | null;
|
|
200
304
|
estimates: Estimates;
|
|
201
305
|
googleSheetRow?: string;
|
|
202
306
|
createdAt?: Date;
|
|
@@ -206,7 +310,9 @@ export declare class Content {
|
|
|
206
310
|
sentimentAnalysis?: SentimentAnalysis | null;
|
|
207
311
|
thumbnailUrl?: string;
|
|
208
312
|
mediaUrl?: string;
|
|
313
|
+
/** @deprecated Obsoleto desde a GD-707: a edição vale por parte (manualMetrics). */
|
|
209
314
|
isEdited?: boolean;
|
|
315
|
+
/** @deprecated Obsoleto desde a GD-707. */
|
|
210
316
|
editedAt?: Date;
|
|
211
317
|
followersAtPost?: number | null;
|
|
212
318
|
history?: ContentMetricsHistoryEntry[];
|
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = exports.CONTENT_CLASSIFICATION_SOURCE_VALUES = exports.CONTENT_METRICS_SOURCE_VALUES = exports.CONTENT_COMMENT_COUNT_SOURCE_VALUES = exports.CONTENT_REPORTABLE_STATUS_VALUES = exports.CONTENT_STATUS_VALUES = exports.EstimatesSchema = exports.Estimates = exports.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
|
|
12
|
+
exports.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = exports.CONTENT_CLASSIFICATION_SOURCE_VALUES = exports.CONTENT_METRICS_SOURCE_VALUES = exports.CONTENT_COMMENT_COUNT_SOURCE_VALUES = exports.CONTENT_REPORTABLE_STATUS_VALUES = exports.CONTENT_STATUS_VALUES = exports.EstimatesSchema = exports.Estimates = exports.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.ContentVideoSchema = exports.ContentVideo = exports.ContentMetricsBreakdownSchema = exports.ContentMetricsBreakdown = exports.ContentBaseMetricsSchema = exports.ContentBaseMetrics = exports.CONTENT_BASE_METRIC_KEYS = exports.ContentCollabSchema = exports.ContentCollab = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
|
|
13
13
|
const mongoose_1 = require("@nestjs/mongoose");
|
|
14
14
|
const mongoose_2 = require("mongoose");
|
|
15
15
|
const normalize_content_link_util_1 = require("../utils/normalize-content-link.util");
|
|
@@ -207,11 +207,176 @@ __decorate([
|
|
|
207
207
|
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
208
208
|
__metadata("design:type", Object)
|
|
209
209
|
], Metrics.prototype, "cpe", void 0);
|
|
210
|
+
__decorate([
|
|
211
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
212
|
+
__metadata("design:type", Object)
|
|
213
|
+
], Metrics.prototype, "potentialReach", void 0);
|
|
214
|
+
__decorate([
|
|
215
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
216
|
+
__metadata("design:type", Object)
|
|
217
|
+
], Metrics.prototype, "reachEngagementRate", void 0);
|
|
218
|
+
__decorate([
|
|
219
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
220
|
+
__metadata("design:type", Object)
|
|
221
|
+
], Metrics.prototype, "followersEngagementRate", void 0);
|
|
222
|
+
__decorate([
|
|
223
|
+
(0, mongoose_1.Prop)({ type: String, required: false, default: null }),
|
|
224
|
+
__metadata("design:type", Object)
|
|
225
|
+
], Metrics.prototype, "tier", void 0);
|
|
226
|
+
__decorate([
|
|
227
|
+
(0, mongoose_1.Prop)({ type: Boolean, required: false, default: null }),
|
|
228
|
+
__metadata("design:type", Object)
|
|
229
|
+
], Metrics.prototype, "isViral", void 0);
|
|
210
230
|
Metrics = __decorate([
|
|
211
231
|
(0, mongoose_1.Schema)({ _id: false })
|
|
212
232
|
], Metrics);
|
|
213
233
|
exports.Metrics = Metrics;
|
|
214
234
|
exports.MetricsSchema = mongoose_1.SchemaFactory.createForClass(Metrics);
|
|
235
|
+
/** Coautor do post (Colab), com os seguidores dele no momento do post. Ver GD-703. */
|
|
236
|
+
let ContentCollab = class ContentCollab {
|
|
237
|
+
};
|
|
238
|
+
__decorate([
|
|
239
|
+
(0, mongoose_1.Prop)({ type: mongoose_2.Schema.Types.ObjectId, ref: 'Profile', required: true }),
|
|
240
|
+
__metadata("design:type", Object)
|
|
241
|
+
], ContentCollab.prototype, "profile", void 0);
|
|
242
|
+
__decorate([
|
|
243
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
244
|
+
__metadata("design:type", Object)
|
|
245
|
+
], ContentCollab.prototype, "followersAtPost", void 0);
|
|
246
|
+
ContentCollab = __decorate([
|
|
247
|
+
(0, mongoose_1.Schema)({ _id: false })
|
|
248
|
+
], ContentCollab);
|
|
249
|
+
exports.ContentCollab = ContentCollab;
|
|
250
|
+
exports.ContentCollabSchema = mongoose_1.SchemaFactory.createForClass(ContentCollab);
|
|
251
|
+
/** Chaves brutas de Metrics que têm parte Orgânica e Impulsionada. Ver GD-707. */
|
|
252
|
+
exports.CONTENT_BASE_METRIC_KEYS = [
|
|
253
|
+
'impressionCount',
|
|
254
|
+
'reachCount',
|
|
255
|
+
'likeCount',
|
|
256
|
+
'commentCount',
|
|
257
|
+
'savedCount',
|
|
258
|
+
'sharesCount',
|
|
259
|
+
'storyCount',
|
|
260
|
+
'tapBackCount',
|
|
261
|
+
'clickCount',
|
|
262
|
+
'replyCount',
|
|
263
|
+
'tapForwardCount',
|
|
264
|
+
'swipeForwardCount',
|
|
265
|
+
'exitCount',
|
|
266
|
+
'favoriteCount',
|
|
267
|
+
'dislikeCount',
|
|
268
|
+
'emv',
|
|
269
|
+
];
|
|
270
|
+
let ContentBaseMetrics = class ContentBaseMetrics {
|
|
271
|
+
};
|
|
272
|
+
__decorate([
|
|
273
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
274
|
+
__metadata("design:type", Object)
|
|
275
|
+
], ContentBaseMetrics.prototype, "impressionCount", void 0);
|
|
276
|
+
__decorate([
|
|
277
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
278
|
+
__metadata("design:type", Object)
|
|
279
|
+
], ContentBaseMetrics.prototype, "reachCount", void 0);
|
|
280
|
+
__decorate([
|
|
281
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
282
|
+
__metadata("design:type", Object)
|
|
283
|
+
], ContentBaseMetrics.prototype, "likeCount", void 0);
|
|
284
|
+
__decorate([
|
|
285
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
286
|
+
__metadata("design:type", Object)
|
|
287
|
+
], ContentBaseMetrics.prototype, "commentCount", void 0);
|
|
288
|
+
__decorate([
|
|
289
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
290
|
+
__metadata("design:type", Object)
|
|
291
|
+
], ContentBaseMetrics.prototype, "savedCount", void 0);
|
|
292
|
+
__decorate([
|
|
293
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
294
|
+
__metadata("design:type", Object)
|
|
295
|
+
], ContentBaseMetrics.prototype, "sharesCount", void 0);
|
|
296
|
+
__decorate([
|
|
297
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
298
|
+
__metadata("design:type", Object)
|
|
299
|
+
], ContentBaseMetrics.prototype, "storyCount", void 0);
|
|
300
|
+
__decorate([
|
|
301
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
302
|
+
__metadata("design:type", Object)
|
|
303
|
+
], ContentBaseMetrics.prototype, "tapBackCount", void 0);
|
|
304
|
+
__decorate([
|
|
305
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
306
|
+
__metadata("design:type", Object)
|
|
307
|
+
], ContentBaseMetrics.prototype, "clickCount", void 0);
|
|
308
|
+
__decorate([
|
|
309
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
310
|
+
__metadata("design:type", Object)
|
|
311
|
+
], ContentBaseMetrics.prototype, "replyCount", void 0);
|
|
312
|
+
__decorate([
|
|
313
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
314
|
+
__metadata("design:type", Object)
|
|
315
|
+
], ContentBaseMetrics.prototype, "tapForwardCount", void 0);
|
|
316
|
+
__decorate([
|
|
317
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
318
|
+
__metadata("design:type", Object)
|
|
319
|
+
], ContentBaseMetrics.prototype, "swipeForwardCount", void 0);
|
|
320
|
+
__decorate([
|
|
321
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
322
|
+
__metadata("design:type", Object)
|
|
323
|
+
], ContentBaseMetrics.prototype, "exitCount", void 0);
|
|
324
|
+
__decorate([
|
|
325
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
326
|
+
__metadata("design:type", Object)
|
|
327
|
+
], ContentBaseMetrics.prototype, "favoriteCount", void 0);
|
|
328
|
+
__decorate([
|
|
329
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
330
|
+
__metadata("design:type", Object)
|
|
331
|
+
], ContentBaseMetrics.prototype, "dislikeCount", void 0);
|
|
332
|
+
__decorate([
|
|
333
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
334
|
+
__metadata("design:type", Object)
|
|
335
|
+
], ContentBaseMetrics.prototype, "emv", void 0);
|
|
336
|
+
ContentBaseMetrics = __decorate([
|
|
337
|
+
(0, mongoose_1.Schema)({ _id: false })
|
|
338
|
+
], ContentBaseMetrics);
|
|
339
|
+
exports.ContentBaseMetrics = ContentBaseMetrics;
|
|
340
|
+
exports.ContentBaseMetricsSchema = mongoose_1.SchemaFactory.createForClass(ContentBaseMetrics);
|
|
341
|
+
/** Divisão Orgânico e Impulsionado; o total em Content.metrics é sempre a soma. Ver GD-707. */
|
|
342
|
+
let ContentMetricsBreakdown = class ContentMetricsBreakdown {
|
|
343
|
+
};
|
|
344
|
+
__decorate([
|
|
345
|
+
(0, mongoose_1.Prop)({ type: exports.ContentBaseMetricsSchema, required: false, default: () => ({}) }),
|
|
346
|
+
__metadata("design:type", ContentBaseMetrics)
|
|
347
|
+
], ContentMetricsBreakdown.prototype, "organic", void 0);
|
|
348
|
+
__decorate([
|
|
349
|
+
(0, mongoose_1.Prop)({ type: exports.ContentBaseMetricsSchema, required: false, default: () => ({}) }),
|
|
350
|
+
__metadata("design:type", ContentBaseMetrics)
|
|
351
|
+
], ContentMetricsBreakdown.prototype, "boosted", void 0);
|
|
352
|
+
ContentMetricsBreakdown = __decorate([
|
|
353
|
+
(0, mongoose_1.Schema)({ _id: false })
|
|
354
|
+
], ContentMetricsBreakdown);
|
|
355
|
+
exports.ContentMetricsBreakdown = ContentMetricsBreakdown;
|
|
356
|
+
exports.ContentMetricsBreakdownSchema = mongoose_1.SchemaFactory.createForClass(ContentMetricsBreakdown);
|
|
357
|
+
/**
|
|
358
|
+
* Dados do vídeo para o VCR. Views não ficam aqui: impressões e views são
|
|
359
|
+
* sempre Content.metrics.impressionCount. Ver GD-707.
|
|
360
|
+
*/
|
|
361
|
+
let ContentVideo = class ContentVideo {
|
|
362
|
+
};
|
|
363
|
+
__decorate([
|
|
364
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
365
|
+
__metadata("design:type", Object)
|
|
366
|
+
], ContentVideo.prototype, "duration", void 0);
|
|
367
|
+
__decorate([
|
|
368
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
369
|
+
__metadata("design:type", Object)
|
|
370
|
+
], ContentVideo.prototype, "watchTime", void 0);
|
|
371
|
+
__decorate([
|
|
372
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
373
|
+
__metadata("design:type", Object)
|
|
374
|
+
], ContentVideo.prototype, "vcr", void 0);
|
|
375
|
+
ContentVideo = __decorate([
|
|
376
|
+
(0, mongoose_1.Schema)({ _id: false })
|
|
377
|
+
], ContentVideo);
|
|
378
|
+
exports.ContentVideo = ContentVideo;
|
|
379
|
+
exports.ContentVideoSchema = mongoose_1.SchemaFactory.createForClass(ContentVideo);
|
|
215
380
|
let ContentMetricsHistoryEntry = class ContentMetricsHistoryEntry {
|
|
216
381
|
};
|
|
217
382
|
__decorate([
|
|
@@ -391,15 +556,25 @@ __decorate([
|
|
|
391
556
|
__metadata("design:type", Boolean)
|
|
392
557
|
], Content.prototype, "hasAffiliateLink", void 0);
|
|
393
558
|
__decorate([
|
|
394
|
-
(0, mongoose_1.Prop)([
|
|
395
|
-
{ type: mongoose_2.Schema.Types.ObjectId, ref: 'Profile', required: false },
|
|
396
|
-
]),
|
|
559
|
+
(0, mongoose_1.Prop)({ type: [exports.ContentCollabSchema], required: false, default: [] }),
|
|
397
560
|
__metadata("design:type", Array)
|
|
398
561
|
], Content.prototype, "collabs", void 0);
|
|
399
562
|
__decorate([
|
|
400
563
|
(0, mongoose_1.Prop)({ type: exports.MetricsSchema, required: true }),
|
|
401
564
|
__metadata("design:type", Metrics)
|
|
402
565
|
], Content.prototype, "metrics", void 0);
|
|
566
|
+
__decorate([
|
|
567
|
+
(0, mongoose_1.Prop)({ type: exports.ContentMetricsBreakdownSchema, required: false, default: null }),
|
|
568
|
+
__metadata("design:type", Object)
|
|
569
|
+
], Content.prototype, "metricsBreakdown", void 0);
|
|
570
|
+
__decorate([
|
|
571
|
+
(0, mongoose_1.Prop)({ type: [String], required: false, default: [] }),
|
|
572
|
+
__metadata("design:type", Array)
|
|
573
|
+
], Content.prototype, "manualMetrics", void 0);
|
|
574
|
+
__decorate([
|
|
575
|
+
(0, mongoose_1.Prop)({ type: exports.ContentVideoSchema, required: false, default: null }),
|
|
576
|
+
__metadata("design:type", Object)
|
|
577
|
+
], Content.prototype, "video", void 0);
|
|
403
578
|
__decorate([
|
|
404
579
|
(0, mongoose_1.Prop)({ type: exports.MetricsSchema, required: true }),
|
|
405
580
|
__metadata("design:type", Estimates)
|
|
@@ -500,6 +675,8 @@ exports.ContentSchema.index({ createdAt: -1, status: 1 });
|
|
|
500
675
|
// Trava de aprovação entre Ações (GD-699): busca o mesmo post por publicação ou link.
|
|
501
676
|
exports.ContentSchema.index({ primetagId: 1 });
|
|
502
677
|
exports.ContentSchema.index({ linkKey: 1 });
|
|
678
|
+
// Visão por Influenciador (GD-703): posts em que o Perfil é coautor.
|
|
679
|
+
exports.ContentSchema.index({ 'collabs.profile': 1 });
|
|
503
680
|
// Chave de idempotência da captura da Primetag — conteúdos manuais (sem
|
|
504
681
|
// primetagId) ficam fora do índice.
|
|
505
682
|
exports.ContentSchema.index({ action: 1, primetagId: 1 }, { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ContentMetricsBreakdown, Metrics } from '../schemas/content.schema';
|
|
2
|
+
/**
|
|
3
|
+
* Totais de Content.metrics a partir da divisão Orgânico e Impulsionado. Ver GD-707.
|
|
4
|
+
*
|
|
5
|
+
* - metrics.<chave> = organic + boosted (null só se as duas forem null);
|
|
6
|
+
* - engagementCount = o da Primetag, a menos que alguma parte de curtidas,
|
|
7
|
+
* comentários, compartilhamentos ou salvos seja manual: aí vira a soma dos totais;
|
|
8
|
+
* - engagementRate = engagementCount ÷ impressionCount.
|
|
9
|
+
*
|
|
10
|
+
* Usada pela captura e pela edição (api-campaigns): as duas precisam dar o mesmo total.
|
|
11
|
+
*/
|
|
12
|
+
export declare const mergeMetricsBreakdown: (breakdown: ContentMetricsBreakdown, primetagEngagementCount: number | null | undefined, manualMetrics?: string[]) => Partial<Metrics>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeMetricsBreakdown = void 0;
|
|
4
|
+
const content_schema_1 = require("../schemas/content.schema");
|
|
5
|
+
// Partes que, se digitadas à mão, fazem o engajamento virar a soma.
|
|
6
|
+
const ENGAGEMENT_KEYS = ['likeCount', 'commentCount', 'sharesCount', 'savedCount'];
|
|
7
|
+
const sumParts = (organic, boosted) => organic == null && boosted == null ? null : (organic ?? 0) + (boosted ?? 0);
|
|
8
|
+
/**
|
|
9
|
+
* Totais de Content.metrics a partir da divisão Orgânico e Impulsionado. Ver GD-707.
|
|
10
|
+
*
|
|
11
|
+
* - metrics.<chave> = organic + boosted (null só se as duas forem null);
|
|
12
|
+
* - engagementCount = o da Primetag, a menos que alguma parte de curtidas,
|
|
13
|
+
* comentários, compartilhamentos ou salvos seja manual: aí vira a soma dos totais;
|
|
14
|
+
* - engagementRate = engagementCount ÷ impressionCount.
|
|
15
|
+
*
|
|
16
|
+
* Usada pela captura e pela edição (api-campaigns): as duas precisam dar o mesmo total.
|
|
17
|
+
*/
|
|
18
|
+
const mergeMetricsBreakdown = (breakdown, primetagEngagementCount, manualMetrics = []) => {
|
|
19
|
+
const organic = breakdown.organic ?? {};
|
|
20
|
+
const boosted = breakdown.boosted ?? {};
|
|
21
|
+
const totals = {};
|
|
22
|
+
for (const key of content_schema_1.CONTENT_BASE_METRIC_KEYS) {
|
|
23
|
+
totals[key] = sumParts(organic[key], boosted[key]);
|
|
24
|
+
}
|
|
25
|
+
const hasManualEngagement = ENGAGEMENT_KEYS.some((key) => manualMetrics.includes(`organic.${key}`) || manualMetrics.includes(`boosted.${key}`));
|
|
26
|
+
const engagementCount = hasManualEngagement
|
|
27
|
+
? ENGAGEMENT_KEYS.reduce((sum, key) => sum + (totals[key] ?? 0), 0)
|
|
28
|
+
: primetagEngagementCount ?? null;
|
|
29
|
+
const impressionCount = totals.impressionCount ?? 0;
|
|
30
|
+
return {
|
|
31
|
+
...totals,
|
|
32
|
+
engagementCount,
|
|
33
|
+
engagementRate: engagementCount != null && impressionCount > 0 ? engagementCount / impressionCount : null,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
exports.mergeMetricsBreakdown = mergeMetricsBreakdown;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -157,10 +157,155 @@ export class Metrics {
|
|
|
157
157
|
|
|
158
158
|
@Prop({ type: Number, required: false, default: null })
|
|
159
159
|
cpe?: number | null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Alcance Potencial (o antigo PO Reach): seguidores no momento do post. Em
|
|
163
|
+
* Content.metrics soma o autor e todos os coautores. Ver GD-703.
|
|
164
|
+
*/
|
|
165
|
+
@Prop({ type: Number, required: false, default: null })
|
|
166
|
+
potentialReach?: number | null;
|
|
167
|
+
|
|
168
|
+
/** Tx. de Engaj. por Alcance: engagementCount ÷ reachCount (fração). Ver GD-703. */
|
|
169
|
+
@Prop({ type: Number, required: false, default: null })
|
|
170
|
+
reachEngagementRate?: number | null;
|
|
171
|
+
|
|
172
|
+
/** Tx. de Engaj. por Seguidores: engagementCount ÷ potentialReach (fração). Ver GD-703. */
|
|
173
|
+
@Prop({ type: Number, required: false, default: null })
|
|
174
|
+
followersEngagementRate?: number | null;
|
|
175
|
+
|
|
176
|
+
/** Nome do Client.tiers pelo potentialReach do post; null sem Tiers ou fora das faixas. Ver GD-703. */
|
|
177
|
+
@Prop({ type: String, required: false, default: null })
|
|
178
|
+
tier?: string | null;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* followersEngagementRate ≥ meta viralReachEngagementRate do Tier (nome da
|
|
182
|
+
* meta herdado da GD-687); null sem Tier ou sem a meta. Ver GD-703.
|
|
183
|
+
*/
|
|
184
|
+
@Prop({ type: Boolean, required: false, default: null })
|
|
185
|
+
isViral?: boolean | null;
|
|
160
186
|
}
|
|
161
187
|
|
|
162
188
|
export const MetricsSchema = SchemaFactory.createForClass(Metrics);
|
|
163
189
|
|
|
190
|
+
/** Coautor do post (Colab), com os seguidores dele no momento do post. Ver GD-703. */
|
|
191
|
+
@Schema({ _id: false })
|
|
192
|
+
export class ContentCollab {
|
|
193
|
+
@Prop({ type: MongooseSchema.Types.ObjectId, ref: 'Profile', required: true })
|
|
194
|
+
profile: Types.ObjectId | Profile;
|
|
195
|
+
|
|
196
|
+
@Prop({ type: Number, required: false, default: null })
|
|
197
|
+
followersAtPost?: number | null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const ContentCollabSchema = SchemaFactory.createForClass(ContentCollab);
|
|
201
|
+
|
|
202
|
+
/** Chaves brutas de Metrics que têm parte Orgânica e Impulsionada. Ver GD-707. */
|
|
203
|
+
export const CONTENT_BASE_METRIC_KEYS = [
|
|
204
|
+
'impressionCount',
|
|
205
|
+
'reachCount',
|
|
206
|
+
'likeCount',
|
|
207
|
+
'commentCount',
|
|
208
|
+
'savedCount',
|
|
209
|
+
'sharesCount',
|
|
210
|
+
'storyCount',
|
|
211
|
+
'tapBackCount',
|
|
212
|
+
'clickCount',
|
|
213
|
+
'replyCount',
|
|
214
|
+
'tapForwardCount',
|
|
215
|
+
'swipeForwardCount',
|
|
216
|
+
'exitCount',
|
|
217
|
+
'favoriteCount',
|
|
218
|
+
'dislikeCount',
|
|
219
|
+
'emv',
|
|
220
|
+
] as const;
|
|
221
|
+
export type ContentBaseMetricKey = (typeof CONTENT_BASE_METRIC_KEYS)[number];
|
|
222
|
+
|
|
223
|
+
@Schema({ _id: false })
|
|
224
|
+
export class ContentBaseMetrics {
|
|
225
|
+
@Prop({ type: Number, required: false, default: null })
|
|
226
|
+
impressionCount?: number | null;
|
|
227
|
+
|
|
228
|
+
@Prop({ type: Number, required: false, default: null })
|
|
229
|
+
reachCount?: number | null;
|
|
230
|
+
|
|
231
|
+
@Prop({ type: Number, required: false, default: null })
|
|
232
|
+
likeCount?: number | null;
|
|
233
|
+
|
|
234
|
+
@Prop({ type: Number, required: false, default: null })
|
|
235
|
+
commentCount?: number | null;
|
|
236
|
+
|
|
237
|
+
@Prop({ type: Number, required: false, default: null })
|
|
238
|
+
savedCount?: number | null;
|
|
239
|
+
|
|
240
|
+
@Prop({ type: Number, required: false, default: null })
|
|
241
|
+
sharesCount?: number | null;
|
|
242
|
+
|
|
243
|
+
@Prop({ type: Number, required: false, default: null })
|
|
244
|
+
storyCount?: number | null;
|
|
245
|
+
|
|
246
|
+
@Prop({ type: Number, required: false, default: null })
|
|
247
|
+
tapBackCount?: number | null;
|
|
248
|
+
|
|
249
|
+
@Prop({ type: Number, required: false, default: null })
|
|
250
|
+
clickCount?: number | null;
|
|
251
|
+
|
|
252
|
+
@Prop({ type: Number, required: false, default: null })
|
|
253
|
+
replyCount?: number | null;
|
|
254
|
+
|
|
255
|
+
@Prop({ type: Number, required: false, default: null })
|
|
256
|
+
tapForwardCount?: number | null;
|
|
257
|
+
|
|
258
|
+
@Prop({ type: Number, required: false, default: null })
|
|
259
|
+
swipeForwardCount?: number | null;
|
|
260
|
+
|
|
261
|
+
@Prop({ type: Number, required: false, default: null })
|
|
262
|
+
exitCount?: number | null;
|
|
263
|
+
|
|
264
|
+
@Prop({ type: Number, required: false, default: null })
|
|
265
|
+
favoriteCount?: number | null;
|
|
266
|
+
|
|
267
|
+
@Prop({ type: Number, required: false, default: null })
|
|
268
|
+
dislikeCount?: number | null;
|
|
269
|
+
|
|
270
|
+
@Prop({ type: Number, required: false, default: null })
|
|
271
|
+
emv?: number | null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const ContentBaseMetricsSchema = SchemaFactory.createForClass(ContentBaseMetrics);
|
|
275
|
+
|
|
276
|
+
/** Divisão Orgânico e Impulsionado; o total em Content.metrics é sempre a soma. Ver GD-707. */
|
|
277
|
+
@Schema({ _id: false })
|
|
278
|
+
export class ContentMetricsBreakdown {
|
|
279
|
+
@Prop({ type: ContentBaseMetricsSchema, required: false, default: () => ({}) })
|
|
280
|
+
organic: ContentBaseMetrics;
|
|
281
|
+
|
|
282
|
+
@Prop({ type: ContentBaseMetricsSchema, required: false, default: () => ({}) })
|
|
283
|
+
boosted: ContentBaseMetrics;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const ContentMetricsBreakdownSchema = SchemaFactory.createForClass(ContentMetricsBreakdown);
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Dados do vídeo para o VCR. Views não ficam aqui: impressões e views são
|
|
290
|
+
* sempre Content.metrics.impressionCount. Ver GD-707.
|
|
291
|
+
*/
|
|
292
|
+
@Schema({ _id: false })
|
|
293
|
+
export class ContentVideo {
|
|
294
|
+
/** Duração em segundos. */
|
|
295
|
+
@Prop({ type: Number, required: false, default: null })
|
|
296
|
+
duration?: number | null;
|
|
297
|
+
|
|
298
|
+
/** Tempo total assistido, somando todas as visualizações, em segundos. */
|
|
299
|
+
@Prop({ type: Number, required: false, default: null })
|
|
300
|
+
watchTime?: number | null;
|
|
301
|
+
|
|
302
|
+
/** (watchTime ÷ duration) ÷ impressionCount, sem teto. */
|
|
303
|
+
@Prop({ type: Number, required: false, default: null })
|
|
304
|
+
vcr?: number | null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const ContentVideoSchema = SchemaFactory.createForClass(ContentVideo);
|
|
308
|
+
|
|
164
309
|
@Schema({ _id: false })
|
|
165
310
|
export class ContentMetricsHistoryEntry {
|
|
166
311
|
@Prop({ type: MetricsSchema, required: true })
|
|
@@ -330,14 +475,27 @@ export class Content {
|
|
|
330
475
|
@Prop({ type: Boolean, required: true, default: false })
|
|
331
476
|
hasAffiliateLink: boolean;
|
|
332
477
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
]
|
|
336
|
-
collabs?: Types.ObjectId[] | Profile[];
|
|
478
|
+
/** Coautores do post; isCollab = collabs.length > 0. Ver GD-703. */
|
|
479
|
+
@Prop({ type: [ContentCollabSchema], required: false, default: [] })
|
|
480
|
+
collabs?: ContentCollab[];
|
|
337
481
|
|
|
482
|
+
/** Métricas do post (sem métrica por pessoa): derivadas com o potentialReach somado. Ver GD-703. */
|
|
338
483
|
@Prop({ type: MetricsSchema, required: true })
|
|
339
484
|
metrics: Metrics;
|
|
340
485
|
|
|
486
|
+
@Prop({ type: ContentMetricsBreakdownSchema, required: false, default: null })
|
|
487
|
+
metricsBreakdown?: ContentMetricsBreakdown | null;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Caminhos das partes digitadas à mão (ex.: organic.reachCount,
|
|
491
|
+
* video.watchTime): a captura não sobrescreve. Ver GD-707.
|
|
492
|
+
*/
|
|
493
|
+
@Prop({ type: [String], required: false, default: [] })
|
|
494
|
+
manualMetrics?: string[];
|
|
495
|
+
|
|
496
|
+
@Prop({ type: ContentVideoSchema, required: false, default: null })
|
|
497
|
+
video?: ContentVideo | null;
|
|
498
|
+
|
|
341
499
|
@Prop({ type: MetricsSchema, required: true })
|
|
342
500
|
estimates: Estimates;
|
|
343
501
|
|
|
@@ -368,9 +526,11 @@ export class Content {
|
|
|
368
526
|
@Prop({ type: String, required: false })
|
|
369
527
|
mediaUrl?: string;
|
|
370
528
|
|
|
529
|
+
/** @deprecated Obsoleto desde a GD-707: a edição vale por parte (manualMetrics). */
|
|
371
530
|
@Prop({ type: Boolean, required: false, default: false })
|
|
372
531
|
isEdited?: boolean;
|
|
373
532
|
|
|
533
|
+
/** @deprecated Obsoleto desde a GD-707. */
|
|
374
534
|
@Prop({ type: Date, required: false })
|
|
375
535
|
editedAt?: Date;
|
|
376
536
|
|
|
@@ -437,6 +597,8 @@ ContentSchema.index({ createdAt: -1, status: 1 });
|
|
|
437
597
|
// Trava de aprovação entre Ações (GD-699): busca o mesmo post por publicação ou link.
|
|
438
598
|
ContentSchema.index({ primetagId: 1 });
|
|
439
599
|
ContentSchema.index({ linkKey: 1 });
|
|
600
|
+
// Visão por Influenciador (GD-703): posts em que o Perfil é coautor.
|
|
601
|
+
ContentSchema.index({ 'collabs.profile': 1 });
|
|
440
602
|
// Chave de idempotência da captura da Primetag — conteúdos manuais (sem
|
|
441
603
|
// primetagId) ficam fora do índice.
|
|
442
604
|
ContentSchema.index(
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTENT_BASE_METRIC_KEYS,
|
|
3
|
+
ContentBaseMetricKey,
|
|
4
|
+
ContentBaseMetrics,
|
|
5
|
+
ContentMetricsBreakdown,
|
|
6
|
+
Metrics,
|
|
7
|
+
} from '../schemas/content.schema';
|
|
8
|
+
|
|
9
|
+
// Partes que, se digitadas à mão, fazem o engajamento virar a soma.
|
|
10
|
+
const ENGAGEMENT_KEYS: ContentBaseMetricKey[] = ['likeCount', 'commentCount', 'sharesCount', 'savedCount'];
|
|
11
|
+
|
|
12
|
+
const sumParts = (organic?: number | null, boosted?: number | null): number | null =>
|
|
13
|
+
organic == null && boosted == null ? null : (organic ?? 0) + (boosted ?? 0);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Totais de Content.metrics a partir da divisão Orgânico e Impulsionado. Ver GD-707.
|
|
17
|
+
*
|
|
18
|
+
* - metrics.<chave> = organic + boosted (null só se as duas forem null);
|
|
19
|
+
* - engagementCount = o da Primetag, a menos que alguma parte de curtidas,
|
|
20
|
+
* comentários, compartilhamentos ou salvos seja manual: aí vira a soma dos totais;
|
|
21
|
+
* - engagementRate = engagementCount ÷ impressionCount.
|
|
22
|
+
*
|
|
23
|
+
* Usada pela captura e pela edição (api-campaigns): as duas precisam dar o mesmo total.
|
|
24
|
+
*/
|
|
25
|
+
export const mergeMetricsBreakdown = (
|
|
26
|
+
breakdown: ContentMetricsBreakdown,
|
|
27
|
+
primetagEngagementCount: number | null | undefined,
|
|
28
|
+
manualMetrics: string[] = [],
|
|
29
|
+
): Partial<Metrics> => {
|
|
30
|
+
const organic: ContentBaseMetrics = breakdown.organic ?? {};
|
|
31
|
+
const boosted: ContentBaseMetrics = breakdown.boosted ?? {};
|
|
32
|
+
|
|
33
|
+
const totals: Partial<Metrics> = {};
|
|
34
|
+
for (const key of CONTENT_BASE_METRIC_KEYS) {
|
|
35
|
+
totals[key] = sumParts(organic[key], boosted[key]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const hasManualEngagement = ENGAGEMENT_KEYS.some(
|
|
39
|
+
(key) => manualMetrics.includes(`organic.${key}`) || manualMetrics.includes(`boosted.${key}`),
|
|
40
|
+
);
|
|
41
|
+
const engagementCount = hasManualEngagement
|
|
42
|
+
? ENGAGEMENT_KEYS.reduce((sum, key) => sum + (totals[key] ?? 0), 0)
|
|
43
|
+
: primetagEngagementCount ?? null;
|
|
44
|
+
|
|
45
|
+
const impressionCount = totals.impressionCount ?? 0;
|
|
46
|
+
return {
|
|
47
|
+
...totals,
|
|
48
|
+
engagementCount,
|
|
49
|
+
engagementRate: engagementCount != null && impressionCount > 0 ? engagementCount / impressionCount : null,
|
|
50
|
+
};
|
|
51
|
+
};
|