Appearance
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
| Library | Use |
|---|---|
| Jak Guru Typescript App Starter | Application Scaffolding and Toolkit |
| @nhtio/logger | Logging |
| bentocache | Redis-based Cache |
| knex | SQL Query Builder |
| @nhtio/kyoo | AMQP Queue |
| verrou | Transaction Locking |
| luxon | DateTime 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:
| Variable | Type | Description |
|---|---|---|
LOG_LEVEL | enum('emerg', 'alert', 'crit', 'error', 'warning', 'notice', 'info', 'debug') | Determines the minimum level of logs to output to the console |
LOKI_HOST | string | The optional hostname of the Grafana Loki server to push logs to |
LOKI_BASIC_AUTH | string | The optional basic authentication credentials to access Grafana Loki over HTTP |
DB_HOST | string | Defines the hostname or IP address of the database server |
DB_PORT | number | Defines the port which the database server is listening for connections on |
DB_USER | string | Defines the username used to authenticate connections to the database server |
DB_PASSWORD | string | Defines the password used to authentication connections to the database server |
DB_NAME | string | The name of the database used by the application in the database server |
DB_SECURE | boolean | Defines whether the connection to the database server requires SSL/TLS |
REDIS_HOST | string | Defines the hostname or IP address of the redis server |
REDIS_PORT | number | Defines the port which the redis server is listening for connections on |
REDIS_DB | number | Defines the redis DB ID used by the application in the redis server |
REDIS_KEY_PREFIX | string | Defines a prefix for all keys stored in the redis database for the application |
REDIS_TLS | boolean | Defines whether the connection to the redis server requires SSL/TLS |
KYOO_AMQP_PROTOCOL | enum('amqp', 'amqps') | Defines the AMQP protocol to use for the RabbitMQ connection |
KYOO_AMQP_HOSTNAME | string | Defines the AMQP broker host |
KYOO_AMQP_PORT | number | Defines port on which the AMQP broker is listening for requests on |
KYOO_AMQP_USERNAME | string | Defines the username for authenticating with the AMQP broker |
KYOO_AMQP_PASSWORD | string | Defines the password for authenticating with the AMQP broker |
KYOO_AMQP_VHOST | string | Defines the AMQP virtual host |
KYOO_AMQP_EXCHANGE | string | Defines 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
}Related Schemas
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;
}Related Links for Tenant Configuration Schemas
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
EvaluatableObject (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>
}Related Links for CRM Vendor Adapters
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>
}Related Links for Interaction Integration Adapters
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:
| Channel | Method |
|---|---|
chat | parseChatRequest |
ticket | parseTicketRequest |
voice | parseVoiceRequest |
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
| Argument | Source |
|---|---|
url | NormalizedIncomingJobPayload.url |
headers | NormalizedIncomingJobPayload.headers |
payload | NormalizedIncomingJobPayload.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:
| Property | Source |
|---|---|
tenant | TenantConfiguration.key |
channel | NormalizedIncomingJobPayload.channel |
clientId | Evaluatable.client.id |
invitedAt | null |
createdAt | NormalizedIncomingJobPayload.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 Count | Action |
|---|---|
| 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=trueSending 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
| Argument | Source |
|---|---|
evaluatable | Evaluatable |
channel | TenantConfiguration.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:
| Task | Est. Hours | Notes |
|---|---|---|
| Application Scaffolding | 5 | Based on Typescript App Starter |
| Observability Implementation | 3 | @nhtio/logger, Sentry for NodeJS |
| Kyo͞o Integration | 5 | Setting up as a consumer |
| Database Implementation | 5 | Schemas + Migrations |
| Survey Vendor Adapters | 10 | |
| CRM Vendor Adapters | 24 | |
| Interaction Integration Adapters | 16 | |
| Job Runtime Implementation | 8 | |
| Graceful Shutdown | 2 | Ensures that unless serving, the scripts shutdown cleanly once requests are complete |
| Automated QA Test Creation | 14 | General End to End Functionality Testing |
| Containerization and Packaging | 4 | Dockerization, Building for Serverless, CI/CD Configuration |