Skip to content

Commit e5eb0fc

Browse files
committed
NMS-16318: User Data Collection
1 parent e393831 commit e5eb0fc

File tree

9 files changed

+254
-8
lines changed

9 files changed

+254
-8
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ node_modules
3434

3535
# WebStorm
3636
.idea
37-
dist
37+
dist
38+
39+
# This file should NOT be checked in, it will be added in production
40+
# environments with credentials populated
41+
config/production.json

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ curl -v -X POST --location "http://localhost:9200/opennms_system/_count?pretty"
4848

4949
## Running in the production environment
5050

51+
Add a file `config/production.json` which is a copy of `config/default.json` but with actual production values populated.
52+
53+
* Note: make sure not to check in `config/production.json` as it may contain credentials.
54+
55+
Then:
56+
57+
```shell
58+
npm run start:prod
59+
```
60+
61+
If you are running this is in a different way (e.g. as part of a service), you can run `npm build-only` to build. Make sure to set the `NODE_ENV` environment variable to `production` (`export NODE_ENV=production`), then run the `dist/app.js` file:
62+
5163
```shell
52-
npm start
64+
export NODE_ENV=production
65+
node dist/app.js
5366
```
67+

config/default.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"crmConfig": {
3+
"crmBaseUrl": "http://localhost",
4+
"crmUrlPath": "/crm",
5+
"portalId": "123",
6+
"formGuid": "abc",
7+
"subscriptionTypeId": "111"
8+
}
9+
}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
"dependencies": {
77
"axios": "^1.6.5",
88
"axios-retry": "^4.0.0",
9+
"config": "^3.3.10",
910
"express": "^4.17.1"
1011
},
1112
"devDependencies": {
13+
"@types/config": "^3.3.3",
1214
"@types/express": "^4.17.13",
1315
"@types/node": "^16.7.10",
1416
"@typescript-eslint/eslint-plugin": "^6.19.1",
@@ -17,7 +19,9 @@
1719
"typescript": "^5.3.3"
1820
},
1921
"scripts": {
22+
"build-only": "tsc -p .",
2023
"start": "tsc -p . && node dist/app.js",
24+
"start:prod": "tsc -p . && export NODE_ENV=production && node dist/app.js",
2125
"test": "tsc --noEmit --strict -p . && eslint ."
2226
},
2327
"author": "Jesse White <jesse@opennms.org>",

src/app.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import express from "express";
2-
import type {ErrorRequestHandler} from "express";
3-
import {Elastic} from "./elastic";
1+
import express from 'express';
2+
import type { ErrorRequestHandler } from 'express';
3+
import { Elastic } from './elastic';
4+
import config from 'config'
5+
import { postCrmData, validateConfig } from './lib/userDataCollection/service';
6+
import { CrmConfig } from './lib/userDataCollection/types';
47

58
const port = 3542;
69
const app = express();
@@ -15,7 +18,6 @@ const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
1518
res.writeHead(500, headers);
1619
res.end("");
1720
next();
18-
1921
}
2022
app.use(errorHandler);
2123

@@ -40,8 +42,34 @@ app.post("/hs-usage-report", async (req, res) => {
4042
res.end();
4143
});
4244

45+
/**
46+
* Receive user data from OpenNMS product (e.g. Horizon), send to CRM.
47+
*/
48+
app.post("/user-data-collection", async (req, res) => {
49+
const crmConfig = config.get<CrmConfig>('crmConfig');
50+
51+
if (!validateConfig(crmConfig)) {
52+
console.error('Invalid CRM configuration', crmConfig);
53+
res.writeHead(500, headers);
54+
res.end();
55+
}
56+
57+
const data = req.body;
58+
console.log("User data collection data received: ", data);
59+
60+
let statusCode = 200;
61+
62+
try {
63+
await postCrmData(crmConfig, data);
64+
} catch (e) {
65+
console.error('Error received posting CRM data: ', e);
66+
statusCode = 500;
67+
}
68+
69+
res.writeHead(statusCode, headers);
70+
res.end();
71+
});
72+
4373
app.listen(port, () => {
4474
console.log(`server is listening on ${port}`);
4575
})
46-
47-

src/lib/userDataCollection/parser.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
CrmDataField,
3+
CrmJsonData,
4+
FormContext,
5+
FormData,
6+
LegalConsentCommunication,
7+
LegalConsentOptions
8+
} from './types'
9+
10+
const CONSENT_TEXT = 'I agree to receive email communications from OpenNMS';
11+
const LEGAL_CONSENT_COMMUNICATION_TEXT = 'If you consent to us contacting you, please opt in below. We will maintain your data until you request us to delete it from our systems. You may opt out of receiving communications from us at any time.';
12+
const CONTACT_OBJECT_ID = '0-1';
13+
14+
export const parseCrmData = (formData: FormData, subscriptionTypeId: string): CrmJsonData => {
15+
const context = {
16+
pageName: 'OpenNMS Web Console',
17+
pageUri: 'opennms/index.jsp'
18+
} as FormContext;
19+
20+
const communication = {
21+
value: true,
22+
subscriptionTypeId,
23+
text: LEGAL_CONSENT_COMMUNICATION_TEXT
24+
} as LegalConsentCommunication;
25+
26+
const legalConsentOptions = {
27+
consent: {
28+
consentToProcess: true,
29+
text: CONSENT_TEXT,
30+
communications: [communication]
31+
}
32+
} as LegalConsentOptions;
33+
34+
const fieldValues = [
35+
{ name: 'firstname', value: formData.firstName },
36+
{ name: 'lastname', value: formData.lastName },
37+
{ name: 'email', value: formData.email },
38+
{ name: 'company', value: formData.company },
39+
{ name: 'horizon_system_id', value: formData.systemId },
40+
{ name: 'utm_campaign', value: 'Horizon' },
41+
{ name: 'utm_medium', value: 'direct' },
42+
{ name: 'utm_content', value: 'Horizon IForm' },
43+
{ name: 'utm_term', value: '' }
44+
];
45+
46+
// UTC epoch milliseconds
47+
const submittedAt = Date.now();
48+
49+
const jsonData = {
50+
fields: fieldValues.map(({ name, value }) => createField(name, value)),
51+
submittedAt,
52+
context,
53+
legalConsentOptions
54+
} as CrmJsonData;
55+
56+
return jsonData;
57+
}
58+
59+
const createField = (name: string, value: string) => {
60+
return {
61+
objectTypeId: CONTACT_OBJECT_ID,
62+
name,
63+
value
64+
} as CrmDataField;
65+
}

src/lib/userDataCollection/service.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { CrmConfig, CrmJsonData, FormData } from './types';
2+
import { parseCrmData } from './parser';
3+
import { Httpclient } from '../../httpclient';
4+
5+
export const postCrmData = async (config: CrmConfig, data: FormData) => {
6+
if (!validateFormData(data)) {
7+
console.error('Invalid form data received', data);
8+
throw new Error('Invalid form data');
9+
}
10+
11+
const crmData: CrmJsonData = parseCrmData(data, config.subscriptionTypeId);
12+
const client = new Httpclient(config.crmBaseUrl);
13+
const url = `${config.crmUrlPath}/${config.portalId}/${config.formGuid}`
14+
15+
try {
16+
await client.post(url, crmData);
17+
18+
console.log(`Successfully posted User Data Collection CRM data to url '${config.crmBaseUrl}${url}'`);
19+
} catch (e) {
20+
console.error(`Failed to post User Data Collection CRM data report to url '${config.crmBaseUrl}${url}'`, e);
21+
throw e;
22+
}
23+
}
24+
25+
const validateFormData = (data: FormData): boolean => {
26+
if (!data ||
27+
!data.consent ||
28+
!data.email ||
29+
!data.product ||
30+
data.product.toLowerCase() !== 'horizon') {
31+
return false;
32+
}
33+
34+
return true;
35+
}
36+
37+
export const validateConfig = (config?: CrmConfig): boolean => {
38+
if (!config) {
39+
return false;
40+
}
41+
42+
if (!config.crmBaseUrl ||
43+
!config.crmUrlPath ||
44+
!config.formGuid ||
45+
!config.portalId ||
46+
!config.subscriptionTypeId) {
47+
return false;
48+
}
49+
50+
return true;
51+
}

src/lib/userDataCollection/types.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* User data received from an OpenNMS product instance.
3+
*/
4+
export interface FormData {
5+
product: string; // for now this should be 'Horizon', but could potentially be used for other products
6+
consent: boolean;
7+
firstName: string;
8+
lastName: string;
9+
email: string;
10+
company: string;
11+
systemId: string;
12+
}
13+
14+
export interface CrmDataField {
15+
objectTypeId: string;
16+
name: string;
17+
value: string;
18+
}
19+
20+
export interface FormContext {
21+
pageUri: string;
22+
pageName: string;
23+
}
24+
25+
export interface LegalConsentCommunication {
26+
value: boolean;
27+
subscriptionTypeId: string;
28+
text: string;
29+
}
30+
31+
export interface LegalConsent {
32+
communications: LegalConsentCommunication[];
33+
consentToProcess: boolean;
34+
text: string;
35+
}
36+
37+
export interface LegalConsentOptions {
38+
consent: LegalConsent;
39+
}
40+
41+
/**
42+
* Data to post to the CRM endpoint.
43+
*/
44+
export interface CrmJsonData {
45+
fields: CrmDataField[];
46+
submittedAt: number; // timestamp, epoch in ms
47+
context: FormContext;
48+
legalConsentOptions: LegalConsentOptions;
49+
}
50+
51+
export interface CrmConfig {
52+
crmBaseUrl: string;
53+
crmUrlPath: string;
54+
portalId: string;
55+
formGuid: string;
56+
subscriptionTypeId: string;
57+
}

test/user-data-collection-test.http

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
POST http://localhost:3542/user-data-collection
2+
Content-Type: application/json
3+
4+
{
5+
"product": "Horizon",
6+
"consent": true,
7+
"firstName": "First",
8+
"lastName": "Last",
9+
"email": "someone@example.com",
10+
"company": "OpenNMS Test",
11+
"systemId": "444"
12+
}
13+
14+
###

0 commit comments

Comments
 (0)