Skip to content

Request Processor Specifications

Pending Approval

These specifications are pending final approval and may change

The request processor is an instance of a worker which will accept jobs from the incoming queue and will process them according to combination of tenant, vendor and channel appropriate to the request.

Libraries to be utilized

LibraryUse
Jak Guru Typescript App StarterApplication Scaffolding and Toolkit
@nhtio/loggerLogging
bentocacheRedis-based Cache
knexSQL Query Builder
@nhtio/kyooAMQP Queue
verrouTransaction Locking
luxonDateTime and Durations

Application Configuration

Each instance of the request processor will load its requisite configuration through environmental variables which can be set either in .env files or via the mechanisms provided by container orchestration systems.

In addition to any NodeJS-specific environmental variables, the following environmental variables will be used to configure the instance:

VariableTypeDescription
LOG_LEVELenum('emerg', 'alert', 'crit', 'error', 'warning', 'notice', 'info', 'debug')Determines the minimum level of logs to output to the console
LOKI_HOSTstringThe optional hostname of the Grafana Loki server to push logs to
LOKI_BASIC_AUTHstringThe optional basic authentication credentials to access Grafana Loki over HTTP
DB_HOSTstringDefines the hostname or IP address of the database server
DB_PORTnumberDefines the port which the database server is listening for connections on
DB_USERstringDefines the username used to authenticate connections to the database server
DB_PASSWORDstringDefines the password used to authentication connections to the database server
DB_NAMEstringThe name of the database used by the application in the database server
DB_SECUREbooleanDefines whether the connection to the database server requires SSL/TLS
REDIS_HOSTstringDefines the hostname or IP address of the redis server
REDIS_PORTnumberDefines the port which the redis server is listening for connections on
REDIS_DBnumberDefines the redis DB ID used by the application in the redis server
REDIS_KEY_PREFIXstringDefines a prefix for all keys stored in the redis database for the application
REDIS_TLSbooleanDefines whether the connection to the redis server requires SSL/TLS
KYOO_AMQP_PROTOCOLenum('amqp', 'amqps')Defines the AMQP protocol to use for the RabbitMQ connection
KYOO_AMQP_HOSTNAMEstringDefines the AMQP broker host
KYOO_AMQP_PORTnumberDefines port on which the AMQP broker is listening for requests on
KYOO_AMQP_USERNAMEstringDefines the username for authenticating with the AMQP broker
KYOO_AMQP_PASSWORDstringDefines the password for authenticating with the AMQP broker
KYOO_AMQP_VHOSTstringDefines the AMQP virtual host
KYOO_AMQP_EXCHANGEstringDefines the AMQP message bus exchange

Relational Database Migrations

The Request Processor application will attempt to run the latest database migrations on boot to ensure that the database has the required tables with the required structure to serve the application. This will be faciliated by using the knex migrations API from within the application during the boot phase of the lifecycle.

IMPORTANT

A lock should be implemented on the migration process to ensure that multiple instances of the application do not attempt to run migrations simultaneously.

Tenant Configurations

Tenant configurations will be stored in a relational database for ease of management, cost reduction and ease of implementation.

Schema

The tenant configuration will be stored in the database in such a way that the resulting model, when retreived from the database, will have the following type:

typescript
interface TenantConfiguration {
    /**
     * The key used to identify the tenant in requests
     * @remarks The key should be used as a `unique` index in the database
     */
    key: string
    /**
     * Determine if requests for this tenant
     * should be processed or discarded
     */
    enabled: boolean
    /**
     * Determine if requests for this tenant
     * for livechat interactions
     * should be processed or discarded
     */
    enabledChat: boolean
    /**
     * The delivery channel for the invitation to the
     * survey for livechat interactions
     * Unique<[primary, fallback]>
     */
    deliveryChannelChat: ['email' | 'sms', 'email' | 'sms' | undefined]
    /**
     * Determine if requests for this tenant
     * for helpdesk interactions
     * should be processed or discarded
     */
    enabledTicket: boolean
    /**
     * The delivery channel for the invitation to the
     * survey for helpdesk interactions
     * Unique<[primary, fallback]>
     */
    deliveryChannelTicket: ['email' | 'sms', 'email' | 'sms' | undefined]
    /**
     * Determine if requests for this tenant
     * for voice interactions
     * should be processed or discarded
     */
    enabledVoice: boolean
    /**
     * The delivery channel for the invitation to the
     * survey for phone interactions
     * Unique<[primary, fallback]>
     */
    deliveryChannelVoice: ['email' | 'sms', 'email' | 'sms' | undefined]
    /**
     * Determine if requests for this tenant
     * for the LiveChat Inc integration
     * should be processed or discarded
     */
    liveChatIncEnabled: boolean
    /**
     * The optional configuration for using the LiveChat Inc APIs
     */
    liveChatIncConfiguration: LiveChatConfiguration | null
    /**
     * Determine if requests for this tenant
     * for the Voiso integration
     * should be processed or discarded
     */
    voisoEnabled: boolean
    /**
     * The optional configuration for using the Voiso APIs
     */
    voisoConfiguration: VoisoConfiguration | null
    /**
     * Determines which survey vendor integration
     * should be used by this tenant
     */
    surveyVendor: 'surveyMonkey'
    /**
     * The configuration for using
     * the survey vendor integration
     */
    surveyVendorConfiguration: SurveyVendorConfiguration
    /**
     * Determines which crm vendor integration
     * should be used by this tenant
     */
    crmVendor: 'pandats' | 'teamforce' | 'antelope'
    /**
     * The configuration for using
     * the CRM vendor integration
     */
    crmVendorConfiguration: CRMVendorConfiguration
    /**
     * The timezone used for calendar based calculations
     */
    timezone: string
}
typescript
interface LiveChatConfiguration {
    personalAccessToken: string
}
typescript
interface VoisoConfiguration {
    clientApiKey: string
    userApiKey: string
}
typescript
type SurveyVendorConfiguration = SurveyMonkeyConfiguration

interface SurveyMonkeyConfiguration {
    accessToken: string
    surveyId: number
}
typescript
import type { CRMClientConfig as PandaTSConfiguration } from '@nht-pandats/sdk/clients/crm'
type CRMVendorConfiguration = PandaTSConfiguration | TeamforceConfiguration | AntelopeConfiguration

interface TeamforceConfiguration {
    url: string
    email: string;
    password: string;
}

interface AntelopeConfiguration {
    url: string
    email: string;
    password: string;
}

Tenant Configuration Management

For additional information on tenant configuration management, please review the tenant configuration mangement implementation specifications.

Interaction History

In order to populate the information required to evaluate if a client should recieve a survey or not, a log of previous survey invitations will be kept with the following schema and stored in the database.

typescript
import type { DateTime } from 'luxon'

interface InteractionHistoryRecord {
    tenant: string
    channel: 'chat' | 'ticket' | 'voice'
    clientId: string
    invitedAt: DateTime | null
}

Monitoring and Observability

Logging and observability are used to ensure that the application is operating as expected, and without any major complications or issues. In order to ensure that our monitoring tools and systems are able to inform us when there is an issue, a combination of @nhtio/logger and Sentry for NodeJS will be implemented.

Log Monitoring

In addition to the LOG_LEVEL environmental variable, the logger can be configured to use the Winston Loki Transport by passing at least the LOKI_HOST environmental variable.

Observability

In order to help gain visibility into possible performance issues, unexpected errors or other failures within the application, Sentry for NodeJS will be implemented.

CAUTION

Do not use a Sentry SDK version higher than 7 due to limitations of the NHT Self-Hosted Sentry installation

In order to ensure that sufficient contextual information is available to help pinpoint the origin of an error, Sentry Trace Propagation will be used to connect between a requests's origin within the web service and the attempt of the job which processes that request.

TIP

As this is a single codebase, the Sentry DSN will be hard-coded into the application.

Custom Context Information

In addition to the information needed to enable Sentry Trace Propagation, each job attempt will need to have additional information added to the Sentry Context including:

  • Tenant Configuration (masking sensitive and personally identifiable information such as API keys)
  • Request Information (masking sensitive and personally identifiable information such as names, emails, phone number etc. from the payload)
  • The Evaluatable Object (masking sensitive and personally identifiable information such as names, emails, phone number etc.), once available

Integration Adapters

There are 3 main types of integration adapters which will be created in order to integrate with the various vendors required to faciliate the desired functionality.

Survey Vendor Adapters

Survey Vendor Adapters are used by the job to send surveys to clients.

typescript
import type { DateTime, Duration } from 'luxon'
import type { PandaTsCountryAlpha2 } from '@nht-pandats/sdk/constants/countries'

interface NormalizedCRMClient {
    tenant: string
    id: string
    name: string
    email: string
    phone: string | null
    country: PandaTsCountryAlpha2 | null
}

/**
 * A normalized interface for information about the request 
 */
interface Evaluatable {
    interaction: {
        endedAt: DateTime
        lookbackCount: number
        lookbackDuration: Duration
    },
    client: NormalizedCRMClient
}

/**
 * The shape of the constructor for an instance of a survey vendor adapter
 * @param configuration: The configuration which is used to interact with the APIs of the survey vendor
 */
interface SurveyVendorAdapterConstructor {
    new (configuration: SurveyVendorConfiguration)
}

interface SurveyVendorAdapterInstance {
    /**
     * Send an invitation to the client via the specified channel
     */
    invite(evaluatable: Evaluatable, channel: 'email' | 'sms'): void | Promise<void>
}

CRM Vendor Adapters

CRM Vendor Adapters are used by the job to retrieve information about the client from the CRM which can be used to evaluate eligibility and / or send invitations.

typescript
import type { PandaTsCountryAlpha2 } from '@nht-pandats/sdk/constants/countries'

/**
 * The shape of the constructor for an instance of a survey vendor adapter
 * @param configuration: The configuration which is used to interact with the APIs of the crm vendor
 */
interface CRMVendorAdapterConstructor {
    new (configuration: CRMVendorConfiguration)
}

interface CRMVendorAdapterInstance {
    /**
     * Lookup a client by their account identifier
     */
    findClientByID(id: string): NormalizedCRMClient | undefined | Promise<NormalizedCRMClient | undefined>
    /**
     * Lookup a client by their email address
     */
    findClientByEmail(email: string): NormalizedCRMClient | undefined | Promise<NormalizedCRMClient | undefined>
    /**
     * Lookup a client by their phone number
     */
    findClientByPhoneNumber(phone: string, country: PandaTsCountryAlpha2 | null): NormalizedCRMClient | undefined | Promise<NormalizedCRMClient | undefined>
}

Interaction Integration Adapters

Interaction Integration Adapters are used by the job to parse incoming requests and return the information required in order to decide eligibility.

typescript
/**
 * The shape of the constructor for an instance of an interaction integration adapter
 * @param configuration: The configuration which is used to interact with the APIs of the interaction integration
 * @param crm: An instance of the adapter used to interact with the APIs of the vendor's CRM integration
 */
interface InteractionIntegrationAdapterConstructor {
    new (configuration: LiveChatConfiguration | VoisoConfiguration, crm: CRMIntegrationAdapterInstance, surveyor: SurveyVendorAdapterInstance): InteractionIntegrationAdapterInstance
}

interface InteractionIntegrationAdapterInstance {
    /**
     * Parses a livechat request and returns an evaluatable object 
     */
    parseChatRequest(url: string, headers: Record<string, any>, payload: any): Evaluatable | Promise<Evaluatable>
    /**
     * Parses a helpdesk request and returns an evaluatable object 
     */
    parseTicketRequest(url: string, headers: Record<string, any>, payload: any): Evaluatable | Promise<Evaluatable>
    /**
     * Parses a voice request and returns an evaluatable object
     */
    parseVoiceRequest(url: string, headers: Record<string, any>, payload: any): Evaluatable | Promise<Evaluatable>
}

Job Function Flow

The following chart describes the flow of the job function:

The Incoming Job

The payload of an incoming job will have the following shape:

typescript
import type { KyooSerializable } from '@nhtio/kyoo'
interface IncomingJobPayload {
    tenant: string
    vendor: string
    channel: string
    url: string
    headers: Record<string, string | string[]>
    payload: KyooSerializable
    trace: {
        baggage: string
        'sentry-trace': string
    }
}

Before continuing, it should be validated and normalized into the following shape:

typescript
import type { KyooSerializable } from '@nhtio/kyoo'

interface NormalizedIncomingJobPayload {
    tenant: string
    vendor: 'livechat' | 'voiso'
    channel: 'chat' | 'ticket' | 'voice'
    requestedAt: DateTime
    url: string
    headers: Record<string, string | string[]>
    payload: KyooSerializable,
    trace: {
        baggage: string
        'sentry-trace': string
    }
}

IMPORTANT

If the payload of the incoming job cannot be coerced correctly into a normalized payload due to invalid information, the job attempted should be acked as a successful attempt and should return early.

Loading Tenant Configuration and Integration Adapters

Using the tenant property of the NormalizedIncomingJobPayload, load the tenant configuration from the database.

IMPORTANT

If the tenant cannot be found or the tenant is not enabled, the job attempted should be acked as a successful attempt and should return early.

Once the TenantConfiguration has been loaded, load the appropriate CRMVendorAdapterInstance and SurveyVendorAdapterInstance.

Finally, load the InteractionIntegrationAdapterInstance.

IMPORTANT

If any of the integrations are disabled or throw errors on creation, the job attempted should be acked as a successful attempt and should return early.

Parsing the Request

Call the appropriate method on the InteractionIntegrationAdapterInstance for the requests's communication channel:

ChannelMethod
chatparseChatRequest
ticketparseTicketRequest
voiceparseVoiceRequest

IMPORTANT

If the parsing function does not return an Evaluatable object or throws an error, the job attempted should be acked as a successful attempt and should return early.

Once the Evaluatable object has been returned a new lock should be created based on the combationation of the Evaluatable's client's tenant & ID.

IMPORTANT

The lock should be set with a duration of 5m to ensure that if an issue arrises where by an unexpected error occurs within the job processing functionality which causes the job to abort early, the job can be re-processed at a later point.

IMPORTANT

A best-attempt effort should be used to ensure that the lock is released at the end of the job processing in order to allow other jobs for the client to be processed

Where to retrieve the argument values from for parse*Request

ArgumentSource
urlNormalizedIncomingJobPayload.url
headersNormalizedIncomingJobPayload.headers
payloadNormalizedIncomingJobPayload.payload

Storing the Interaction History for the Evaluatable object

Before continuing, the record of the interaction's occurance should be stored in the database so that it can be counted for the evaluation process.

It should be created with the following properties:

PropertySource
tenantTenantConfiguration.key
channelNormalizedIncomingJobPayload.channel
clientIdEvaluatable.client.id
invitedAtnull
createdAtNormalizedIncomingJobPayload.requestedAt

Checking for Eligibility

Once the Evaluatable object is available, it should be used to evaluate the eligibility of the client to receive a survey.

IMPORTANT

If evaluation does not pass, the job attempted should be acked as a successful attempt and should return early.

The dispatcher will send surveys based on the number of surveys sent to a unique client identifier within a calendar month as follows:

Contact CountAction
1st📫 Send
2nd🚩 Send if more than 24h since previous contact
3rd❌ Ignore
4th❌ Ignore
5th❌ Ignore
6th❌ Ignore
7th📫 Send (does not respect cooldown)
8th❌ Ignore
9th❌ Ignore
10th📫 Send (does not respect cooldown)
More❌ Ignore

All clients who contact Customer Support will be eligible to receive surveys irrespective of their CRM status, including clients marked as Do Not Contact (DNC).

Summary

All clients contacting Customer Support are eligible to receive surveys, including clients marked as Do Not Contact (DNC). Surveys are dispatched based on contact count within a calendar month:

  • 1st contact: Survey sent immediately.
  • 2nd contact: Survey sent only if more than 24 hours have passed since the last survey.
  • 3rd-6th contacts: No surveys sent.
  • 7th contact: Survey sent immediately (ignoring cooldown).
  • 8th-9th contacts: No surveys sent.
  • 10th contact: Survey sent immediately (ignoring cooldown).
  • More than 10 contacts: No surveys sent.

The count resets at the start of each calendar month.

Based on this eligibility logic, a client will receive a maximum of 3 surveys per calendar month.

⚠️ Important Distinction ⚠️

A calendar month specifically refers to the named months of the Gregorian calendar (e.g., January, February, March). Survey counts reset at the beginning of each calendar month. Because calendar months vary in length and reset points, using a rolling 30-day period as a comparison highlights the potential overlap between two consecutive months. This means it's possible for a client to receive up to 6 surveys within any given 30-day span—3 surveys at the end of one month and another 3 at the start of the next.

Important Note

The calendar month should be determined based on the timezone configured for the tenant

Psuedo-Code Example

typescript
if (cnt === 0) eligible=true
else if (cnt === 1 && now - lastInvited >= 24h) eligible=true
else if (cnt === 6 || cnt === 9) eligible=true

Sending the Survey

Once the client is confirmed as eligible to receive a survey, the SurveyVendorAdapterInstance.invite method should be called.

Where to retrieve the argument values from for SurveyVendorAdapterInstance.invite

ArgumentSource
evaluatableEvaluatable
channelTenantConfiguration.deliveryChannel*

Updating the InteractionHistoryRecord Record

If there are no errors when calling SurveyVendorAdapterInstance.invite, the relevant InteractionHistoryRecord's invitedAt property should be updated to the DateTime.utc() value.

Queue Considerations

The following are some considerations which should be accounted for when dealing with queue-based processing.

Errors vs Failures

When dealing with queue-based processing, application errors should be considered as job failures. When an application error is encountered, it should be reported using the Sentry.captureException method, nacked, and re-enqueued at the end of the queue.

IMPORTANT

This behavior is different than RabbitMQ's default nack and requeue functionality, so it will need to be handled manually.

However, job failures due to failure to meet conditions or constrains such as validation should be handled as successful attempts. When a failure to meet conditions or constrains is encountered the job attempted should be acked as a successful attempt and should return early.

Graceful Error Handling

The worker for processing the queue job attempt should handle errors gracefully by wrapping the logic in a try/catch, and caught errors should be handled as job failures.

Breakdown of Work

The estimate for the effort needed is 96 hours, broken down as follows:

TaskEst. HoursNotes
Application Scaffolding5Based on Typescript App Starter
Observability Implementation3@nhtio/logger, Sentry for NodeJS
Kyo͞o Integration5Setting up as a consumer
Database Implementation5Schemas + Migrations
Survey Vendor Adapters10
CRM Vendor Adapters24
Interaction Integration Adapters16
Job Runtime Implementation8
Graceful Shutdown2Ensures that unless serving, the scripts shutdown cleanly once requests are complete
Automated QA Test Creation14General End to End Functionality Testing
Containerization and Packaging4Dockerization, Building for Serverless, CI/CD Configuration