Initial project
This commit is contained in:
20
.github/workflows/publish.yml
vendored
Normal file
20
.github/workflows/publish.yml
vendored
Normal 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
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/.angular
|
||||
/dist
|
||||
/node_modules
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
||||
85
README.md
85
README.md
@@ -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
43
angular.json
Normal 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
13490
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
19
projects/ngx-pendo-lite/LICENSE
Normal file
19
projects/ngx-pendo-lite/LICENSE
Normal 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.
|
||||
126
projects/ngx-pendo-lite/README.md
Normal file
126
projects/ngx-pendo-lite/README.md
Normal 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
|
||||
50
projects/ngx-pendo-lite/karma.conf.js
Normal file
50
projects/ngx-pendo-lite/karma.conf.js
Normal 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
|
||||
});
|
||||
};
|
||||
7
projects/ngx-pendo-lite/ng-package.json
Normal file
7
projects/ngx-pendo-lite/ng-package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
47
projects/ngx-pendo-lite/package.json
Normal file
47
projects/ngx-pendo-lite/package.json
Normal 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"
|
||||
}
|
||||
11
projects/ngx-pendo-lite/src/lib/index.ts
Normal file
11
projects/ngx-pendo-lite/src/lib/index.ts
Normal 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';
|
||||
250
projects/ngx-pendo-lite/src/lib/ngx-pendo.service.spec.ts
Normal file
250
projects/ngx-pendo-lite/src/lib/ngx-pendo.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
41
projects/ngx-pendo-lite/src/lib/pendo.module.ts
Normal file
41
projects/ngx-pendo-lite/src/lib/pendo.module.ts
Normal 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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
318
projects/ngx-pendo-lite/src/lib/pendo.service.ts
Normal file
318
projects/ngx-pendo-lite/src/lib/pendo.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
7
projects/ngx-pendo-lite/src/public-api.ts
Normal file
7
projects/ngx-pendo-lite/src/public-api.ts
Normal 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';
|
||||
15
projects/ngx-pendo-lite/src/test.ts
Normal file
15
projects/ngx-pendo-lite/src/test.ts
Normal 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(),
|
||||
);
|
||||
37
projects/ngx-pendo-lite/tsconfig.json
Normal file
37
projects/ngx-pendo-lite/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
31
projects/ngx-pendo-lite/tsconfig.lib.json
Normal file
31
projects/ngx-pendo-lite/tsconfig.lib.json
Normal 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"
|
||||
]
|
||||
}
|
||||
10
projects/ngx-pendo-lite/tsconfig.lib.prod.json
Normal file
10
projects/ngx-pendo-lite/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "full"
|
||||
}
|
||||
}
|
||||
15
projects/ngx-pendo-lite/tsconfig.spec.json
Normal file
15
projects/ngx-pendo-lite/tsconfig.spec.json
Normal 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
32
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user