import type { BlipClient } from '../client.ts' import type { EventTrack, NonNullableEventTracking } from '../types/analytics.ts' import type { Identity } from '../types/node.ts' import { type WebhookTypes, webhookTypes } from '../types/webhook.ts' import { uri } from '../utils/uri.ts' import { type ConsumeOptions, Namespace, type SendCommandOptions } from './namespace.ts' export class AnalyticsNamespace extends Namespace { constructor(blipClient: BlipClient, defaultOptions?: SendCommandOptions) { super(blipClient, 'analytics', defaultOptions) } public async track( contact: Identity | undefined, category: string, action: string, extras: Record = {}, opts?: ConsumeOptions, ): Promise { return await this.sendCommand( { method: 'observe', uri: uri`/event-track`, type: 'application/vnd.iris.eventTrack+json', resource: { category, action, contact: contact ? { identity: contact, } : undefined, extras: { ...(contact ? { contactIdentity: contact } : {}), ...extras, }, } satisfies EventTrack, }, opts, ) } public async getCategories( filter?: string, opts?: ConsumeOptions, ): Promise>> { return await this.sendCommand( { method: 'get', uri: uri`/event-track?${{ categoryFilter: filter }}`, }, { collection: true, ...opts, }, ) } public async *streamCategories( filter?: string, opts?: ConsumeOptions, ): AsyncIterable> { const items = this.sendCommand<'get', Array>>( { method: 'get', uri: uri`/event-track?${{ categoryFilter: filter }}`, }, { collection: true, stream: true, ...opts, }, ) const seenCategories = new Set() for await (const item of items) { if (!seenCategories.has(item.category)) { seenCategories.add(item.category) yield item } } } /** @returns Quantity of actions in the category between the start and end date */ public async getCategoryCount( category: string, startDate: Date, endDate: Date, opts?: ConsumeOptions, ): Promise { const { count } = await this.sendCommand<'get', { count: number }>( { method: 'get', uri: uri`/event-track-count/${category}?${{ startDate, endDate }}`, }, opts, ) return count } /** @returns Counts of each action in the category grouped by day between the start and end date */ public async getCategoryActions( category: string, startDate: Date, endDate: Date, opts?: ConsumeOptions, ): Promise>> { return await this.sendCommand( { method: 'get', uri: uri`/event-track/${category}?${{ startDate, endDate }}`, }, { collection: true, ...opts, }, ) } /** @returns Counts of each action in the category grouped by day between the start and end date (streaming) */ public async *streamCategoryActions( category: string, startDate: Date, endDate: Date, opts?: ConsumeOptions, ): AsyncIterable> { const items = this.sendCommand< 'get', Array> >( { method: 'get', uri: uri`/event-track/${category}?${{ startDate, endDate }}`, }, { collection: true, stream: true, ...opts, }, ) const seenActions = new Set() for await (const item of items) { const key = `${item.storageDate}|${item.category}|${item.action}` if (!seenActions.has(key)) { seenActions.add(key) yield item } } } public async getTrackings(category: string, action: string, startDate: Date, endDate: Date, opts?: ConsumeOptions) { const trackings = await this.sendCommand< 'get', Array< Pick & { contact: { // This is a workaround, sometimes Blip returns it on pascal case Identity: Identity } } > >( { method: 'get', uri: uri`/event-track/${category}/${action}?${{ startDate, endDate }}`, }, { collection: true, ...opts, }, ) return trackings.map((tracking) => ({ ...tracking, contact: { identity: tracking.contact?.Identity ?? tracking.contact?.identity ?? tracking.extras.contactIdentity, }, })) } public async *streamTrackings( category: string, action: string, startDate: Date, endDate: Date, opts?: ConsumeOptions, ) { const trackings = this.sendCommand< 'get', Array< Pick & { contact: { // This is a workaround, sometimes Blip returns it on pascal case Identity: Identity } } > >( { method: 'get', uri: uri`/event-track/${category}/${action}?${{ startDate, endDate }}`, }, { collection: true, stream: true, ...opts, }, ) const seenTrackings = new Set() for await (const tracking of trackings) { const normalizedContact = tracking.contact?.Identity ?? tracking.contact?.identity ?? tracking.extras.contactIdentity const key = `${tracking.storageDate}|${normalizedContact}|${tracking.category}|${tracking.action}` if (!seenTrackings.has(key)) { seenTrackings.add(key) yield { ...tracking, contact: { identity: normalizedContact, }, } } } } public async getActiveMessagesFailed( filters?: { startDate?: Date | string endDate?: Date | string campaign?: string template?: string }, opts?: ConsumeOptions, ): Promise< Array<{ sendDateTime: string userId: string templateName?: string campaignName?: string failed: string broad?: string type?: string }> > { return await this.sendCommand( { method: 'get', uri: uri`/active-messages/failed?${filters}`, }, { collection: true, ...opts, }, ) } public async getActiveMessagesStatus( filters?: { startDate?: Date | string endDate?: Date | string campaign?: string template?: string }, opts?: ConsumeOptions, ): Promise< Array<{ sendDateTime: string sent: number received: number consumed: number response: number failed: number }> > { return await this.sendCommand( { method: 'get', uri: uri`/active-messages/status?${filters}`, }, { collection: true, ...opts, }, ) } public async getActiveMessagesFailedCount( filters?: { startDate?: Date | string endDate?: Date | string campaign?: string template?: string }, opts?: ConsumeOptions, ): Promise< Array<{ cloudApiErrorId: number errorMessage: string count: number }> > { return await this.sendCommand( { method: 'get', uri: uri`/active-messages/failed-count?${filters}`, }, { collection: true, ...opts, }, ) } public async getActiveMessagesReplyHour( filters?: { startDate?: Date | string endDate?: Date | string campaign?: string template?: string }, opts?: ConsumeOptions, ): Promise< Array<{ hour: number count: number }> > { return await this.sendCommand( { method: 'get', uri: uri`/active-messages/reply-hour?${filters}`, }, { collection: true, ...opts, }, ) } public async getActiveMessagesTemplateNames( filters?: { startDate?: Date | string endDate?: Date | string filter?: string }, opts?: ConsumeOptions, ): Promise> { return await this.sendCommand( { method: 'get', uri: uri`/active-messages/template-names?${filters}`, }, { collection: true, ...opts, }, ) } public async getActiveMessagesCampaignNames( filters?: { startDate?: Date | string endDate?: Date | string filter?: string }, opts?: ConsumeOptions, ): Promise> { return await this.sendCommand( { method: 'get', uri: uri`/active-messages/campaign-names?${filters}`, }, { collection: true, ...opts, }, ) } public async hasWebhook(url: string, opts?: ConsumeOptions) { const webhooks = await this.getWebhooks(opts) return webhooks.some((webhook) => webhook.url === url) } public async getWebhooks(opts?: ConsumeOptions) { const configurations = await this.blipClient.account.getConfigurations({ ...opts, ownerIdentity: this.identity, }) const webhookUrls = configurations['Webhook.Url']?.split(';') ?? [] const advancedWebhookSettings = JSON.parse(configurations['Webhook.AdvancedSettings'] ?? '[]') type Settings = { settings: { dispatchTypes: Array customHeaders: Record } } return webhookUrls .filter((url) => url) .map((url) => { const settings = advancedWebhookSettings.find( (webhookSettings: Record) => webhookSettings[url]?.settings, )?.[url]?.settings return { url, settings: (settings ?? { dispatchTypes: webhookTypes, customHeaders: {}, }) as Settings['settings'], } }) } /** * It will add the url as an active webhook if it doesn't exist yet * If the url already exists, it will update the settings] */ public async setWebhook( url: string, settings?: { listeners?: Array headers?: Record }, opts?: ConsumeOptions, ) { const configurations = await this.blipClient.account.getConfigurations({ ...opts, ownerIdentity: this.identity, }) let webhookUrls = configurations['Webhook.Url']?.split(';') ?? [] if (!webhookUrls.includes(url)) { webhookUrls = [...webhookUrls, url] } const webhookSettings = { dispatchTypes: settings?.listeners ?? webhookTypes, customHeaders: settings?.headers ?? {}, } const advancedWebhookSettings = JSON.parse(configurations['Webhook.AdvancedSettings'] ?? '[]').filter( (urls: Record) => !urls[url], ) await this.blipClient.account.setConfigurations( { 'Webhook.Url': webhookUrls.join(';'), 'Webhook.IsValid': 'True', 'Webhook.AdvancedSettings': JSON.stringify([ ...advancedWebhookSettings, { [url]: { settings: webhookSettings, }, }, ]), }, { ...opts, ownerIdentity: this.identity }, ) } public async deleteWebhook(url: string, opts?: ConsumeOptions) { const configurations = await this.blipClient.account.getConfigurations({ ...opts, ownerIdentity: this.identity, }) const currentWebhookUrls = configurations['Webhook.Url']?.split(';') ?? [] const advancedWebhookSettings = JSON.parse(configurations['Webhook.AdvancedSettings'] ?? '[]') await this.blipClient.account.setConfigurations( { 'Webhook.Url': currentWebhookUrls.filter((u) => u !== url).join(';'), 'Webhook.AdvancedSettings': JSON.stringify( advancedWebhookSettings.filter((urls: Record) => !urls[url]), ), }, { ...opts, ownerIdentity: this.identity }, ) } }