Initial project

This commit is contained in:
NinjaPug
2025-05-01 17:26:17 -04:00
parent 8b7d7cecb7
commit e88e767d75
24 changed files with 14704 additions and 4 deletions

20
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Publish to NPM
on:
release:
types: [created]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build:lib
- run: cd dist/ngx-pendo-lite && npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/.angular
/dist
/node_modules

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 NinjaPug
Copyright (c) 2025 Christopher Koch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@@ -1,2 +1,83 @@
# ngx-pendo-lite
Pendo Angular wrapper
# ngx-pendo-lite-workspace
This workspace contains the Angular library for integrating Pendo.io analytics into Angular applications.
## Development
### Library
The main library code is in the `projects/ngx-pendo-lite` directory.
### Build
Run `npm run build:lib` to build the library. The build artifacts will be stored in the `dist/ngx-pendo-lite` directory.
### Running unit tests
Run `npm run test:lib` to execute the unit tests for the library via [Karma](https://karma-runner.github.io).
### Publishing
After building the library, you can publish it to npm with:
```bash
npm run publish:lib
```
Or do a dry run first:
```bash
npm run publish:lib:dry
```
## Versioning
The library follows [Semantic Versioning](https://semver.org/).
To bump the version:
```bash
# For patch releases (bug fixes)
npm run version:patch
# For minor releases (new features, backward compatible)
npm run version:minor
# For major releases (breaking changes)
npm run version:major
```
## Features
- Easy integration with Angular's dependency injection
- Type-safe wrapper for all Pendo functionality
- Server-side rendering (SSR) support
- Comprehensive testing
## Library Usage
```typescript
// Import in your app module
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { PendoModule } from 'ngx-pendo-lite';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
PendoModule.forRoot({
apiKey: 'YOUR_PENDO_API_KEY'
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
```
For more details, see the library's own [README](./projects/ngx-pendo-lite/README.md).
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

43
angular.json Normal file
View File

@@ -0,0 +1,43 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ngx-pendo-lite": {
"projectType": "library",
"root": "projects/ngx-pendo-lite",
"sourceRoot": "projects/ngx-pendo-lite/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/ngx-pendo-lite/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/ngx-pendo-lite/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/ngx-pendo-lite/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "projects/ngx-pendo-lite/tsconfig.spec.json",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}

13490
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "ngx-pendo-lite-workspace",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"build:lib": "ng build ngx-pendo-lite",
"test:lib": "ng test ngx-pendo-lite",
"lint:lib": "ng lint ngx-pendo-lite",
"publish:lib": "cd dist/ngx-pendo-lite && npm publish",
"publish:lib:dry": "cd dist/ngx-pendo-lite && npm publish --dry-run",
"version:patch": "cd projects/ngx-pendo-lite && npm version patch",
"version:minor": "cd projects/ngx-pendo-lite && npm version minor",
"version:major": "cd projects/ngx-pendo-lite && npm version major"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.0",
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/router": "^17.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.5.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.0",
"@angular/cli": "^17.1.0",
"@angular/compiler-cli": "^17.1.0",
"ng-packagr": "^17.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.3.3"
}
}

View File

@@ -0,0 +1,19 @@
MIT License
Copyright (c) 2025 Christopher Koch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,126 @@
# ngx-pendo-lite
A lightweight Angular wrapper for Pendo.io analytics integration.
## Installation
```bash
npm install ngx-pendo-lite --save
```
## Setup
### 1. Import the Module
Import the `PendoModule` in your `AppModule` and configure it with your Pendo API key:
```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { PendoModule } from 'ngx-pendo-lite';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
PendoModule.forRoot({
apiKey: 'YOUR_PENDO_API_KEY'
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
```
### 2. Identify Users (after login)
After a user logs in to your application, identify them to Pendo:
```typescript
import { Component } from '@angular/core';
import { PendoService } from 'ngx-pendo-lite';
@Component({
selector: 'app-login',
template: `...`
})
export class LoginComponent {
constructor(private pendoService: PendoService) {}
onLoginSuccess(user: any): void {
this.pendoService.identify(
user.id,
user.organizationId,
{
email: user.email,
fullName: user.fullName,
role: user.role
},
{
name: user.organizationName,
tier: user.subscriptionTier
}
);
}
}
```
### 3. Track Custom Events
Track user interactions and custom events:
```typescript
import { Component } from '@angular/core';
import { PendoService } from 'ngx-pendo-lite';
@Component({
selector: 'app-feature',
template: `...`
})
export class FeatureComponent {
constructor(private pendoService: PendoService) {}
onFeatureUsed(): void {
this.pendoService.track('feature_used', {
featureName: 'example-feature',
timestamp: new Date().toISOString()
});
}
}
```
## API Reference
### PendoService
#### Methods
- `initialize(config: PendoConfig): void` - Initialize the Pendo service
- `identify(visitorId: string, accountId: string, visitorData?: Record<string, any>, accountData?: Record<string, any>): void` - Identify a user and account
- `track(eventName: string, metadata?: Record<string, any>): void` - Track a custom event
- `updateVisitor(visitorData: Record<string, any>): void` - Update visitor information
- `updateAccount(accountData: Record<string, any>): void` - Update account information
- `disable(): void` - Disable Pendo tracking
#### Interfaces
```typescript
interface PendoConfig {
apiKey: string;
visitor?: {
id: string;
[key: string]: any;
};
account?: {
id: string;
[key: string]: any;
};
}
```
## License
MIT

View File

@@ -0,0 +1,50 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, '../../coverage/ngx-pendo-lite'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
singleRun: false,
restartOnFileChange: true
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ngx-pendo-lite",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,47 @@
{
"name": "ngx-pendo-lite",
"version": "1.0.0",
"description": "Angular wrapper for Pendo.io analytics integration",
"author": "Christopher Koch",
"repository": {
"type": "git",
"url": "https://github.com/programmingPug/ngx-pendo-lite"
},
"keywords": [
"angular",
"pendo",
"analytics",
"tracking",
"pendo.io"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/programmingPug/ngx-pendo-lite/issues"
},
"homepage": "https://github.com/programmingPug/ngx-pendo-lite#readme",
"peerDependencies": {
"@angular/common": ">=12.0.0",
"@angular/core": ">=12.0.0"
},
"dependencies": {
"tslib": "^2.5.0"
},
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"types": "./index.d.ts",
"esm2022": "./esm2022/ngx-pendo.mjs",
"esm": "./esm2022/ngx-pendo.mjs",
"default": "./fesm2022/ngx-pendo.mjs"
},
"./package.json": {
"default": "./package.json"
}
},
"module": "fesm2022/ngx-pendo.mjs",
"typings": "index.d.ts",
"type": "module"
}

View File

@@ -0,0 +1,11 @@
/**
* Library internal index file that exports all public components, directives,
* pipes, services, and other entities from the library
*/
// Export the service
export * from './pendo.service';
// Export the module
export * from './pendo.module';

View File

@@ -0,0 +1,250 @@
import { TestBed } from '@angular/core/testing';
import { PLATFORM_ID } from '@angular/core';
import { PendoService, PENDO_CONFIG } from './pendo.service';
describe('PendoService', () => {
let service: PendoService;
// Mock Pendo instance
const mockPendo = {
initialize: jasmine.createSpy('initialize'),
updateOptions: jasmine.createSpy('updateOptions'),
identify: jasmine.createSpy('identify'),
track: jasmine.createSpy('track'),
disableCookies: jasmine.createSpy('disableCookies'),
isReady: jasmine.createSpy('isReady').and.returnValue(true)
};
const mockConfig = {
apiKey: 'test-api-key',
autoLoad: false
};
beforeEach(() => {
// Define mock for window.pendo
Object.defineProperty(window, 'pendo', {
value: mockPendo,
writable: true
});
TestBed.configureTestingModule({
providers: [
PendoService,
{ provide: PLATFORM_ID, useValue: 'browser' },
{ provide: PENDO_CONFIG, useValue: mockConfig }
]
});
service = TestBed.inject(PendoService);
});
afterEach(() => {
// Reset all spies
mockPendo.initialize.calls.reset();
mockPendo.updateOptions.calls.reset();
mockPendo.identify.calls.reset();
mockPendo.track.calls.reset();
mockPendo.disableCookies.calls.reset();
mockPendo.isReady.calls.reset();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('initialize', () => {
it('should initialize Pendo with the provided config', async () => {
await service.initialize();
expect(mockPendo.initialize).toHaveBeenCalledWith({
apiKey: mockConfig.apiKey,
visitor: undefined,
account: undefined
});
});
it('should initialize with visitor and account data when provided', async () => {
const visitorData = { id: 'visitor-1', name: 'Test User' };
const accountData = { id: 'account-1', name: 'Test Account' };
await service.initialize({
apiKey: 'new-api-key',
visitor: visitorData,
account: accountData
});
expect(mockPendo.initialize).toHaveBeenCalledWith({
apiKey: 'new-api-key',
visitor: visitorData,
account: accountData
});
});
});
describe('updateVisitor', () => {
it('should call updateOptions with visitor data', async () => {
await service.initialize();
const visitorData = { id: 'visitor-1', name: 'Test User' };
service.updateVisitor(visitorData);
expect(mockPendo.updateOptions).toHaveBeenCalledWith({
visitor: visitorData
});
});
it('should not call updateOptions if not initialized', () => {
const visitorData = { id: 'visitor-1', name: 'Test User' };
service.updateVisitor(visitorData);
expect(mockPendo.updateOptions).not.toHaveBeenCalled();
});
});
describe('updateAccount', () => {
it('should call updateOptions with account data', async () => {
await service.initialize();
const accountData = { id: 'account-1', name: 'Test Account' };
service.updateAccount(accountData);
expect(mockPendo.updateOptions).toHaveBeenCalledWith({
account: accountData
});
});
it('should not call updateOptions if not initialized', () => {
const accountData = { id: 'account-1', name: 'Test Account' };
service.updateAccount(accountData);
expect(mockPendo.updateOptions).not.toHaveBeenCalled();
});
});
describe('track', () => {
it('should call track with event name and metadata', async () => {
await service.initialize();
const eventName = 'test-event';
const metadata = { property: 'value' };
service.track(eventName, metadata);
expect(mockPendo.track).toHaveBeenCalledWith(eventName, metadata);
});
it('should not call track if not initialized', () => {
const eventName = 'test-event';
service.track(eventName);
expect(mockPendo.track).not.toHaveBeenCalled();
});
});
describe('identify', () => {
it('should call identify with visitor and account data', async () => {
await service.initialize();
const visitorId = 'visitor-1';
const accountId = 'account-1';
const visitorData = { name: 'Test User' };
const accountData = { name: 'Test Account' };
service.identify(visitorId, accountId, visitorData, accountData);
expect(mockPendo.identify).toHaveBeenCalledWith({
visitor: { id: visitorId, ...visitorData },
account: { id: accountId, ...accountData }
});
});
it('should call identify with minimal data', async () => {
await service.initialize();
const visitorId = 'visitor-1';
const accountId = 'account-1';
service.identify(visitorId, accountId);
expect(mockPendo.identify).toHaveBeenCalledWith({
visitor: { id: visitorId },
account: { id: accountId }
});
});
it('should not call identify if not initialized', () => {
const visitorId = 'visitor-1';
const accountId = 'account-1';
service.identify(visitorId, accountId);
expect(mockPendo.identify).not.toHaveBeenCalled();
});
});
describe('disable', () => {
it('should call disableCookies', async () => {
await service.initialize();
service.disable();
expect(mockPendo.disableCookies).toHaveBeenCalled();
});
it('should not call disableCookies if not initialized', () => {
service.disable();
expect(mockPendo.disableCookies).not.toHaveBeenCalled();
});
});
describe('isReady', () => {
it('should return true when Pendo is ready', async () => {
await service.initialize();
expect(service.isReady()).toBe(true);
});
it('should return false when not initialized', () => {
expect(service.isReady()).toBe(false);
});
});
// Test for SSR (server-side rendering) environment
describe('in SSR environment', () => {
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
providers: [
PendoService,
{ provide: PLATFORM_ID, useValue: 'server' }, // Simulate server-side rendering
{ provide: PENDO_CONFIG, useValue: mockConfig }
]
});
service = TestBed.inject(PendoService);
});
it('should not attempt to initialize Pendo', async () => {
await service.initialize();
expect(mockPendo.initialize).not.toHaveBeenCalled();
});
it('should not call any Pendo methods', () => {
service.track('event');
service.identify('visitor', 'account');
service.updateVisitor({ id: 'visitor' });
service.updateAccount({ id: 'account' });
service.disable();
expect(mockPendo.track).not.toHaveBeenCalled();
expect(mockPendo.identify).not.toHaveBeenCalled();
expect(mockPendo.updateOptions).not.toHaveBeenCalled();
expect(mockPendo.disableCookies).not.toHaveBeenCalled();
});
it('should return false for isReady', () => {
expect(service.isReady()).toBe(false);
});
});
});

View File

@@ -0,0 +1,41 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PendoConfig, PendoService, PENDO_CONFIG } from './pendo.service';
/**
* Angular module for Pendo.io integration
*/
@NgModule({
imports: [
CommonModule
],
providers: []
})
export class PendoModule {
/**
* Use this method in your root module to provide and configure the PendoService
* @param config Configuration for Pendo initialization
* @returns ModuleWithProviders configuration for Angular
*/
static forRoot(config: PendoConfig): ModuleWithProviders<PendoModule> {
return {
ngModule: PendoModule,
providers: [
{ provide: PENDO_CONFIG, useValue: config },
PendoService
]
};
}
/**
* Use this method to include the module in feature modules without providing
* the service again (it should only be provided once in the app)
* @returns ModuleWithProviders configuration for Angular feature modules
*/
static forChild(): ModuleWithProviders<PendoModule> {
return {
ngModule: PendoModule,
providers: []
};
}
}

View File

@@ -0,0 +1,318 @@
import { Inject, Injectable, Optional, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
/**
* Interface for visitor data
*/
export interface PendoVisitor {
/** Required unique visitor identifier */
id: string;
/** Any additional visitor properties */
[key: string]: any;
}
/**
* Interface for account data
*/
export interface PendoAccount {
/** Required unique account identifier */
id: string;
/** Any additional account properties */
[key: string]: any;
}
/**
* Pendo initialization configuration options
*/
export interface PendoConfig {
/** Your Pendo API key */
apiKey: string;
/** Optional visitor data */
visitor?: PendoVisitor;
/** Optional account data */
account?: PendoAccount;
/** Optional Pendo script URL. If not provided, uses the default URL with apiKey */
scriptUrl?: string;
/** Whether to load Pendo script automatically. Default is true */
autoLoad?: boolean;
/** Whether to disable cookies. Default is false */
disableCookies?: boolean;
}
/**
* Type definition for the Pendo global object
*/
export interface PendoInstance {
initialize: (options: {
apiKey: string;
visitor?: PendoVisitor;
account?: PendoAccount;
}) => void;
updateOptions: (options: {
visitor?: PendoVisitor;
account?: PendoAccount;
}) => void;
identify: (visitor: { visitor: PendoVisitor; account: PendoAccount }) => void;
track: (eventName: string, metadata?: Record<string, any>) => void;
disableCookies: () => void;
isReady: () => boolean;
}
/**
* Token for providing Pendo configuration
*/
export const PENDO_CONFIG = 'PENDO_CONFIG';
/**
* Angular service wrapper for Pendo.io analytics
*/
@Injectable({
providedIn: 'root',
})
export class PendoService {
/**
* Flag indicating if Pendo has been initialized
*/
private isInitialized = false;
/**
* Reference to the Pendo instance
*/
private pendoInstance?: PendoInstance;
/**
* Configuration options
*/
private config: PendoConfig;
/**
* Script loading promise to prevent multiple script loads
*/
private loadPromise: Promise<void> | null = null;
/**
* @param platformId Angular platform ID for SSR detection
* @param config Optional injected Pendo configuration
*/
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Optional() @Inject(PENDO_CONFIG) config?: PendoConfig
) {
this.config = config || { apiKey: '' };
// Auto-load the script if running in browser and autoLoad is not explicitly disabled
if (
isPlatformBrowser(this.platformId) &&
config?.apiKey &&
(config.autoLoad !== false)
) {
this.loadPendoScript();
}
}
/**
* Loads the Pendo script asynchronously
* @returns Promise that resolves when the script is loaded
*/
public loadPendoScript(): Promise<void> {
// Don't attempt to load in non-browser environments
if (!isPlatformBrowser(this.platformId)) {
return Promise.resolve();
}
// Return existing promise if already loading
if (this.loadPromise) {
return this.loadPromise;
}
this.loadPromise = new Promise<void>((resolve, reject) => {
// Check if Pendo is already loaded
if (typeof window !== 'undefined' && (window as any).pendo) {
this.pendoInstance = (window as any).pendo;
resolve();
return;
}
try {
// Create a script element to load Pendo
const script = document.createElement('script');
script.async = true;
script.defer = true;
// Use custom URL if provided, otherwise construct from API key
script.src = this.config.scriptUrl ||
`https://cdn.pendo.io/agent/static/${this.config.apiKey}/pendo.js`;
script.onload = () => {
// Make Pendo instance accessible
if (typeof window !== 'undefined') {
this.pendoInstance = (window as any).pendo;
resolve();
} else {
reject(new Error('Window is not defined after script load'));
}
};
script.onerror = () => {
reject(new Error('Failed to load Pendo script'));
};
// Append script to the document head
document.head.appendChild(script);
} catch (error) {
reject(error);
}
});
return this.loadPromise;
}
/**
* Initialize Pendo with configuration
* @param config Pendo configuration options
* @returns Promise that resolves when initialization is complete
*/
public async initialize(config?: PendoConfig): Promise<void> {
// Don't attempt to initialize in non-browser environments
if (!isPlatformBrowser(this.platformId)) {
return;
}
// Merge provided config with existing config
if (config) {
this.config = { ...this.config, ...config };
}
// Validate API key
if (!this.config.apiKey) {
console.error('Pendo API key is required');
return;
}
try {
// Ensure script is loaded
await this.loadPendoScript();
// Check if Pendo instance is available
if (!this.pendoInstance) {
throw new Error('Pendo instance not available');
}
// Initialize Pendo
this.pendoInstance.initialize({
apiKey: this.config.apiKey,
visitor: this.config.visitor,
account: this.config.account,
});
// Set initialized flag
this.isInitialized = true;
// Disable cookies if configured
if (this.config.disableCookies) {
this.disable();
}
} catch (error) {
console.error('Failed to initialize Pendo:', error);
}
}
/**
* Updates visitor information
* @param visitorData Visitor data to update
*/
public updateVisitor(visitorData: PendoVisitor): void {
if (!this.isInitialized || !isPlatformBrowser(this.platformId) || !this.pendoInstance) {
return;
}
this.pendoInstance.updateOptions({
visitor: visitorData
});
}
/**
* Updates account information
* @param accountData Account data to update
*/
public updateAccount(accountData: PendoAccount): void {
if (!this.isInitialized || !isPlatformBrowser(this.platformId) || !this.pendoInstance) {
return;
}
this.pendoInstance.updateOptions({
account: accountData
});
}
/**
* Track a custom event in Pendo
* @param eventName Name of the event to track
* @param metadata Optional metadata for the event
*/
public track(eventName: string, metadata?: Record<string, any>): void {
if (!this.isInitialized || !isPlatformBrowser(this.platformId) || !this.pendoInstance) {
return;
}
this.pendoInstance.track(eventName, metadata);
}
/**
* Identify user and account (or update existing)
* @param visitorId Visitor identifier
* @param accountId Account identifier
* @param visitorData Additional visitor metadata
* @param accountData Additional account metadata
*/
public identify(
visitorId: string,
accountId: string,
visitorData?: Record<string, any>,
accountData?: Record<string, any>
): void {
if (!this.isInitialized || !isPlatformBrowser(this.platformId) || !this.pendoInstance) {
return;
}
const visitor: PendoVisitor = {
id: visitorId,
...(visitorData || {})
};
const account: PendoAccount = {
id: accountId,
...(accountData || {})
};
this.pendoInstance.identify({
visitor,
account
});
}
/**
* Disable Pendo tracking by disabling cookies
*/
public disable(): void {
if (!this.isInitialized || !isPlatformBrowser(this.platformId) || !this.pendoInstance) {
return;
}
this.pendoInstance.disableCookies();
}
/**
* Check if Pendo is ready
* @returns boolean indicating if Pendo is ready
*/
public isReady(): boolean {
if (!isPlatformBrowser(this.platformId) || !this.pendoInstance) {
return false;
}
return typeof this.pendoInstance.isReady === 'function'
? this.pendoInstance.isReady()
: this.isInitialized;
}
}

View File

@@ -0,0 +1,7 @@
/*
* Public API Surface of ngx-pendo-lite
* This is the main entry point for the library
*/
// Re-export everything from the lib/index.ts file
export * from './lib/index';

View File

@@ -0,0 +1,15 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);

View File

@@ -0,0 +1,37 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
],
"paths": {
"ngx-pendo-lite": [
"dist/ngx-pendo-lite"
]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,31 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2022"
],
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"enableResourceInlining": true,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true,
"compilationMode": "partial"
},
"exclude": [
"src/test.ts",
"**/*.spec.ts",
"**/*.test.ts"
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "full"
}
}

View File

@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

32
tsconfig.json Normal file
View File

@@ -0,0 +1,32 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"ngx-pendo": [
"./dist/ngx-pendo"
]
},
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}