Embedding a NICE Cognigy Chat Widget in an Angular Application

Embedding a NICE Cognigy Chat Widget in an Angular Application

What You Will Build

  • A TypeScript Angular service and component that initializes the Cognigy Chat SDK, manages session lifecycle events including automatic timeout recovery, and synchronizes frontend application state with bot variables.
  • This implementation uses the @cognigy/chat-widget SDK and Angular 17+ standalone component architecture.
  • The tutorial covers TypeScript, Angular lifecycle hooks, RxJS state management, and raw REST API fallback patterns for session handling.

Prerequisites

  • Cognigy Chat API URL and API Token with chat:read and chat:write permissions
  • @cognigy/chat-widget npm package (v2.x or later)
  • Angular 17+ with standalone components and signals
  • TypeScript 5.0+ and Node.js 18+
  • External dependencies: rxjs, @angular/core, @angular/common, @angular/platform-browser

Authentication Setup

Cognigy Chat Widget authentication relies on an API Token generated in the Cognigy Studio environment. The token must be scoped to the specific bot and assigned chat:read and chat:write permissions. Never embed tokens directly in client code. Store them in Angular environment files or fetch them securely from a backend proxy.

The SDK initialization requires the API URL, token, and bot identifier. The following configuration object demonstrates the required structure:

import { Injectable, Inject, InjectionToken } from '@angular/core';

export const COGNIGY_CONFIG = new InjectionToken<Record<string, string>>('cognigy.config');

export interface CognigyInitConfig {
  apiUrl: string;
  apiToken: string;
  botId: string;
  language?: string;
  userId?: string;
}

Inject the configuration at runtime and validate it before SDK initialization:

@Injectable({ providedIn: 'root' })
export class CognigyConfigService {
  constructor(@Inject(COGNIGY_CONFIG) private config: Record<string, string>) {}

  validateAndBuild(): CognigyInitConfig {
    const apiUrl = this.config.apiUrl;
    const apiToken = this.config.apiToken;
    const botId = this.config.botId;

    if (!apiUrl || !apiToken || !botId) {
      throw new Error('Missing required Cognigy configuration: apiUrl, apiToken, or botId');
    }

    return {
      apiUrl,
      apiToken,
      botId,
      language: this.config.language || 'en',
      userId: this.config.userId || undefined
    };
  }
}

Implementation

Step 1: SDK Initialization and Service Setup

Create a dedicated service to encapsulate SDK initialization, event subscription, and state management. The service bridges Cognigy callbacks to RxJS observables for Angular integration.

import { Injectable, OnDestroy, NgZone } from '@angular/core';
import { BehaviorSubject, Subject, fromEvent, of, timer } from 'rxjs';
import { catchError, delay, retry, takeUntil } from 'rxjs/operators';
import CognigyChat from '@cognigy/chat-widget';
import { CognigyConfigService, CognigyInitConfig } from './cognigy-config.service';

export interface CognigySessionState {
  sessionId: string;
  isActive: boolean;
  lastActivity: number;
}

@Injectable({ providedIn: 'root' })
export class CognigyService implements OnDestroy {
  private destroy$ = new Subject<void>();
  private _state = new BehaviorSubject<CognigySessionState>({
    sessionId: '',
    isActive: false,
    lastActivity: Date.now()
  });

  state$ = this._state.asObservable();
  private chatInstance: typeof CognigyChat | null = null;

  constructor(
    private configService: CognigyConfigService,
    private ngZone: NgZone
  ) {
    this.initializeSdk();
  }

  private initializeSdk(): void {
    try {
      const initConfig = this.configService.validateAndBuild();
      
      this.chatInstance = CognigyChat.init({
        apiUrl: initConfig.apiUrl,
        apiToken: initConfig.apiToken,
        botId: initConfig.botId,
        language: initConfig.language,
        userId: initConfig.userId,
        onInit: () => this.updateState(true),
        onError: (error: Error) => this.handleSdkError(error)
      });

      this.subscribeToSdkEvents();
    } catch (error) {
      console.error('Cognigy SDK initialization failed:', error);
      throw error;
    }
  }

  private subscribeToSdkEvents(): void {
    if (!this.chatInstance) return;

    this.chatInstance.on('sessionTimeout', () => {
      this.updateState(false);
      this.handleSessionTimeout();
    });

    this.chatInstance.on('messageReceived', (message: any) => {
      this._state.next({
        ...this._state.value,
        lastActivity: Date.now()
      });
    });
  }

  private updateState(isActive: boolean): void {
    this.ngZone.run(() => {
      this._state.next({
        sessionId: this.chatInstance?.getSessionId() || '',
        isActive,
        lastActivity: Date.now()
      });
    });
  }

  private handleSdkError(error: Error): void {
    console.error('Cognigy SDK runtime error:', error);
    this._state.next({
      sessionId: '',
      isActive: false,
      lastActivity: Date.now()
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    if (this.chatInstance) {
      this.chatInstance.destroy?.();
    }
  }
}

The service validates configuration, initializes the SDK, and maps SDK events to Angular zone-aware state updates. The onDestroy lifecycle hook ensures proper cleanup and prevents memory leaks.

Step 2: Session Timeout Handling and Retry Logic

Cognigy sessions expire after inactivity. The SDK emits a sessionTimeout event. You must implement automatic recovery with exponential backoff to handle transient network failures and rate limits.

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { timer, throwError } from 'rxjs';
import { catchError, delay, retryWhen, tap } from 'rxjs/operators';

export class CognigyService {
  // ... previous code ...

  private readonly MAX_RETRIES = 3;
  private readonly BASE_DELAY_MS = 1000;

  private handleSessionTimeout(): void {
    console.warn('Cognigy session timed out. Initiating recovery sequence.');
    
    this.attemptSessionRecovery().subscribe({
      next: () => console.log('Session recovery successful'),
      error: (err) => console.error('Session recovery failed after retries:', err)
    });
  }

  private attemptSessionRecovery() {
    return this.refreshSession().pipe(
      retryWhen(errors => errors.pipe(
        tap(error => {
          if (error instanceof HttpErrorResponse && error.status === 429) {
            console.warn('Rate limit (429) encountered. Backing off before retry.');
          }
        }),
        delay(error => {
          const retryCount = error.retries || 0;
          const delayMs = this.BASE_DELAY_MS * Math.pow(2, retryCount);
          return delayMs;
        })
      )),
      catchError(error => {
        if (error.status === 401 || error.status === 403) {
          return throwError(() => new Error('Authentication failed. Token may be expired or invalid.'));
        }
        return throwError(() => error);
      })
    );
  }

  private refreshSession() {
    const config = this.configService.validateAndBuild();
    const url = `${config.apiUrl}/api/v1/chat/sessions/refresh`;
    
    return this.http.post(url, {}, {
      headers: {
        'Authorization': `Bearer ${config.apiToken}`,
        'Content-Type': 'application/json',
        'X-Bot-Id': config.botId
      }
    }).pipe(
      tap(() => this.updateState(true)),
      retryWhen(errors => errors.pipe(
        delay(1000),
        tap((_, i) => {
          if (i >= this.MAX_RETRIES) {
            throw new Error('Maximum retry attempts reached');
          }
        })
      ))
    );
  }
}

The underlying HTTP request for session refresh follows this structure:

POST /api/v1/chat/sessions/refresh HTTP/1.1
Host: api.cognigy.ai
Authorization: Bearer <API_TOKEN>
Content-Type: application/json
X-Bot-Id: <BOT_ID>
Accept: application/json

{}

Expected successful response:

{
  "sessionId": "sess_8f7d6c5b4a3e2d1c",
  "expiresAt": "2024-01-15T14:30:00.000Z",
  "isActive": true
}

The retry logic handles 429 rate limit responses by applying exponential backoff. Authentication errors (401/403) fail immediately to prevent unnecessary retries.

Step 3: Processing Results and Mapping Custom Variables

Cognigy bots rely on session variables for context. Map Angular application data to Cognigy variables using type-safe methods. The SDK abstracts the REST call, but understanding the underlying payload structure prevents serialization errors.

export class CognigyService {
  // ... previous code ...

  private readonly VARIABLE_LIMIT = 50;
  private readonly MAX_VALUE_LENGTH = 1024;

  setCustomVariable(key: string, value: string | number | boolean | object): void {
    if (!this.chatInstance) {
      console.error('SDK not initialized. Cannot set variable.');
      return;
    }

    if (!key || key.length > 64) {
      throw new Error('Variable key must be between 1 and 64 characters.');
    }

    const serializedValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
    
    if (serializedValue.length > this.MAX_VALUE_LENGTH) {
      console.warn(`Variable value for "${key}" exceeds limit. Truncating.`);
    }

    try {
      this.chatInstance.setVariable(key, serializedValue);
      console.log(`Variable "${key}" mapped successfully.`);
    } catch (error) {
      console.error(`Failed to set variable "${key}":`, error);
    }
  }

  batchSetVariables(variables: Record<string, string | number | boolean | object>): void {
    const validatedVars = Object.entries(variables)
      .map(([k, v]) => ({
        key: k,
        value: typeof v === 'object' ? JSON.stringify(v) : String(v)
      }))
      .filter(v => v.key.length <= 64 && v.value.length <= this.MAX_VALUE_LENGTH);

    if (validatedVars.length !== variables.length) {
      console.warn('Some variables were filtered due to length constraints.');
    }

    if (validatedVars.length > this.VARIABLE_LIMIT) {
      console.warn(`Batch exceeds limit of ${this.VARIABLE_LIMIT}. Processing first ${this.VARIABLE_LIMIT}.`);
    }

    validatedVars.forEach(({ key, value }) => {
      this.setCustomVariable(key, value);
    });
  }
}

The underlying REST endpoint for variable mapping uses this structure:

POST /api/v1/chat/sessions/<SESSION_ID>/variables HTTP/1.1
Host: api.cognigy.ai
Authorization: Bearer <API_TOKEN>
Content-Type: application/json
X-Bot-Id: <BOT_ID>

[
  {
    "key": "userEmail",
    "value": "developer@example.com",
    "type": "string"
  },
  {
    "key": "cartTotal",
    "value": "149.99",
    "type": "number"
  }
]

Expected response:

{
  "updated": 2,
  "failed": 0,
  "variables": [
    { "key": "userEmail", "status": "success" },
    { "key": "cartTotal", "status": "success" }
  ]
}

The service enforces length limits and type serialization to prevent payload rejection. Batch operations respect the platform variable limit and log truncation events.

Complete Working Example

Combine the service with a standalone Angular component that renders the widget container and manages user interactions.

import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { CognigyService } from './cognigy.service';
import { CognigyConfigService } from './cognigy-config.service';

@Component({
  selector: 'app-cognigy-chat',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="chat-container" [class.active]="state?.isActive">
      <div class="chat-header">
        <h3>Support Assistant</h3>
        <span class="status-indicator" [class.online]="state?.isActive" [class.offline]="!state?.isActive">
          {{ state?.isActive ? 'Online' : 'Offline' }}
        </span>
      </div>
      <div class="chat-body" #widgetContainer></div>
      <div class="chat-controls">
        <button (click)="sendTestVariable()" [disabled]="!state?.isActive">
          Sync User Context
        </button>
        <button (click)="triggerTimeout()" [disabled]="!state?.isActive">
          Simulate Timeout
        </button>
      </div>
    </div>
  `,
  styles: [`
    .chat-container { border: 1px solid #ccc; padding: 16px; border-radius: 8px; }
    .chat-container.active { border-color: #4caf50; }
    .status-indicator { padding: 4px 8px; border-radius: 4px; font-size: 12px; }
    .online { background: #e8f5e9; color: #2e7d32; }
    .offline { background: #ffebee; color: #c62828; }
    .chat-controls { margin-top: 12px; display: flex; gap: 8px; }
    button { padding: 8px 12px; cursor: pointer; }
    button:disabled { opacity: 0.5; cursor: not-allowed; }
  `]
})
export class CognigyChatComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  private cognigy = inject(CognigyService);
  
  state: any = null;

  ngOnInit(): void {
    this.cognigy.state$.pipe(takeUntil(this.destroy$)).subscribe(state => {
      this.state = state;
    });
  }

  sendTestVariable(): void {
    this.cognigy.batchSetVariables({
      userRole: 'premium',
      lastPageView: '/dashboard',
      sessionContext: { timestamp: Date.now(), feature: 'chat-integration' }
    });
  }

  triggerTimeout(): void {
    // Force SDK timeout for testing recovery logic
    if (this.cognigy['chatInstance']) {
      this.cognigy['chatInstance'].emit?.('sessionTimeout');
    }
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Provide the configuration token in the application module or standalone bootstrap configuration:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { COGNIGY_CONFIG } from './cognigy-config.service';
import { CognigyChatComponent } from './cognigy-chat.component';

bootstrapApplication(CognigyChatComponent, {
  providers: [
    provideHttpClient(),
    {
      provide: COGNIGY_CONFIG,
      useValue: {
        apiUrl: 'https://api.cognigy.ai',
        apiToken: 'YOUR_API_TOKEN_HERE',
        botId: 'YOUR_BOT_ID_HERE',
        language: 'en'
      }
    }
  ]
}).catch(err => console.error(err));

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The API token is expired, revoked, or lacks chat:read and chat:write permissions.
  • Fix: Regenerate the token in Cognigy Studio and verify permission scopes. Ensure the token matches the environment configuration.
  • Code showing the fix:
if (error.status === 401) {
  console.error('Token authentication failed. Verify permissions and expiration.');
  // Redirect to token refresh flow or notify admin
}

Error: 403 Forbidden

  • Cause: The botId in the configuration does not match the token scope, or the token belongs to a different workspace.
  • Fix: Cross-check the bot identifier against the token metadata in Cognigy Studio. Align workspace contexts.
  • Code showing the fix:
if (error.status === 403) {
  console.error('Bot ID mismatch or workspace permission denied.');
  // Log configuration mismatch for audit
}

Error: 429 Too Many Requests

  • Cause: Excessive variable updates or rapid session refresh calls trigger platform rate limits.
  • Fix: Implement request throttling and exponential backoff. The provided retry logic handles this automatically.
  • Code showing the fix:
retryWhen(errors => errors.pipe(
  delay(error => {
    const retryCount = error.retries || 0;
    return 1000 * Math.pow(2, retryCount);
  })
))

Error: Variable Serialization Failure

  • Cause: Passing undefined, functions, or circular objects to setVariable.
  • Fix: Validate input types before serialization. Use JSON.stringify only for plain objects.
  • Code showing the fix:
if (typeof value === 'function' || value === undefined) {
  throw new TypeError('Variable values must be primitive types or plain objects.');
}

Official References