@oas-tools/oas-telemetry 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +200 -200
- package/README.md +133 -133
- package/dist/exporters/InMemoryDbExporter.cjs +32 -31
- package/dist/index.cjs +56 -19
- package/dist/telemetry.cjs +0 -0
- package/dist/ui.cjs +387 -97
- package/package.json +71 -69
- package/src/exporters/InMemoryDbExporter.js +175 -174
- package/src/index.js +307 -255
- package/src/telemetry.js +25 -25
- package/src/ui.js +387 -97
package/package.json
CHANGED
|
@@ -1,69 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@oas-tools/oas-telemetry",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "This package exports an Express middleware that traces requests and responses of an Express application using OpenTelemetry.",
|
|
5
|
-
"author": "Manuel Otero",
|
|
6
|
-
"contributors": [
|
|
7
|
-
"Alejandro Santisteban",
|
|
8
|
-
"Pablo Fernandez"
|
|
9
|
-
],
|
|
10
|
-
"license": "Apache-2.0",
|
|
11
|
-
"type": "module",
|
|
12
|
-
"scripts": {
|
|
13
|
-
"devImportUi": "node dev/ui/importUiToHtml.js",
|
|
14
|
-
"devExportUi": "node dev/ui/exportHtmlToUi.js",
|
|
15
|
-
"build": "babel src -d dist --out-file-extension .cjs",
|
|
16
|
-
"devUi": "nodemon --watch dev/ui --ext html --exec \"npm run devExportUi && npm run build && node test/functional/otTestServer.cjs\"",
|
|
17
|
-
"publish": "npm run build && npm publish",
|
|
18
|
-
"testPerformance": "npm i && cd test/performance/ && ./test.sh",
|
|
19
|
-
"pretest": "npm run build",
|
|
20
|
-
"test": "npm run testCJS && npm run testMJS",
|
|
21
|
-
"testCJS": "npm run preTestCJS && npm run launchTestCJS && npm run postTestCJS",
|
|
22
|
-
"preTestCJS": "node test/functional/otTestServer.cjs &",
|
|
23
|
-
"launchTestCJS": "npx -y wait-on http://localhost:3000/api/v1/pets && npm run sendRequests && sleep 4 && npm run checkRequestsLog",
|
|
24
|
-
"postTestCJS": "kill `ps -uax | grep \"node test/functional/otTestServer\" | grep -v \"grep\" | grep -v \"sh\" | awk '{print $2}'`",
|
|
25
|
-
"testMJS": "npm run preTestMJS && npm run launchTestMJS && npm run postTestMJS",
|
|
26
|
-
"preTestMJS": "node test/functional/otTestServer.mjs &",
|
|
27
|
-
"launchTestMJS": "npx -y wait-on http://localhost:3000/api/v1/pets && npm run sendRequests && sleep 4 && npm run checkRequestsLog",
|
|
28
|
-
"postTestMJS": "kill `ps -uax | grep \"node test/functional/otTestServer\" | grep -v \"grep\" | grep -v \"sh\" | awk '{print $2}'`",
|
|
29
|
-
"sendRequests": "npx -y newman run test/functional/request-collection.json -e test/functional/request-environment.json",
|
|
30
|
-
"checkRequestsLog": "npx -y newman run test/functional/request-collection-check.json -e test/functional/request-environment.json",
|
|
31
|
-
"launchKSApi": "npm run build; (cd test/performance/ks-api ; node indexTelemetry.js)",
|
|
32
|
-
"launchDevel": "export OTDEBUG=true && npm run launchKSApi"
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"dist",
|
|
36
|
-
"src"
|
|
37
|
-
],
|
|
38
|
-
"main": "./dist/index.cjs",
|
|
39
|
-
"module": "./src/index.js",
|
|
40
|
-
"exports": {
|
|
41
|
-
"require": "./dist/index.cjs",
|
|
42
|
-
"import": "./src/index.js",
|
|
43
|
-
"default": "./src/index.js"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"@opentelemetry/instrumentation-http": "^0.51.0",
|
|
47
|
-
"@opentelemetry/resources": "^1.24.0",
|
|
48
|
-
"@opentelemetry/sdk-node": "^0.49.1",
|
|
49
|
-
"axios": "^1.6.8",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"@babel/
|
|
60
|
-
"@babel/
|
|
61
|
-
"@babel/
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@oas-tools/oas-telemetry",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "This package exports an Express middleware that traces requests and responses of an Express application using OpenTelemetry.",
|
|
5
|
+
"author": "Manuel Otero",
|
|
6
|
+
"contributors": [
|
|
7
|
+
"Alejandro Santisteban",
|
|
8
|
+
"Pablo Fernandez"
|
|
9
|
+
],
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"devImportUi": "node dev/ui/importUiToHtml.js",
|
|
14
|
+
"devExportUi": "node dev/ui/exportHtmlToUi.js",
|
|
15
|
+
"build": "babel src -d dist --out-file-extension .cjs",
|
|
16
|
+
"devUi": "nodemon --watch dev/ui --ext html --exec \"npm run devExportUi && npm run build && node test/functional/otTestServer.cjs\"",
|
|
17
|
+
"publish": "npm run build && npm publish",
|
|
18
|
+
"testPerformance": "npm i && cd test/performance/ && ./test.sh",
|
|
19
|
+
"pretest": "npm run build",
|
|
20
|
+
"test": "npm run testCJS && npm run testMJS",
|
|
21
|
+
"testCJS": "npm run preTestCJS && npm run launchTestCJS && npm run postTestCJS",
|
|
22
|
+
"preTestCJS": "node test/functional/otTestServer.cjs &",
|
|
23
|
+
"launchTestCJS": "npx -y wait-on http://localhost:3000/api/v1/pets && npm run sendRequests && sleep 4 && npm run checkRequestsLog",
|
|
24
|
+
"postTestCJS": "kill `ps -uax | grep \"node test/functional/otTestServer\" | grep -v \"grep\" | grep -v \"sh\" | awk '{print $2}'`",
|
|
25
|
+
"testMJS": "npm run preTestMJS && npm run launchTestMJS && npm run postTestMJS",
|
|
26
|
+
"preTestMJS": "node test/functional/otTestServer.mjs &",
|
|
27
|
+
"launchTestMJS": "npx -y wait-on http://localhost:3000/api/v1/pets && npm run sendRequests && sleep 4 && npm run checkRequestsLog",
|
|
28
|
+
"postTestMJS": "kill `ps -uax | grep \"node test/functional/otTestServer\" | grep -v \"grep\" | grep -v \"sh\" | awk '{print $2}'`",
|
|
29
|
+
"sendRequests": "npx -y newman run test/functional/request-collection.json -e test/functional/request-environment.json",
|
|
30
|
+
"checkRequestsLog": "npx -y newman run test/functional/request-collection-check.json -e test/functional/request-environment.json",
|
|
31
|
+
"launchKSApi": "npm run build; (cd test/performance/ks-api ; node indexTelemetry.js)",
|
|
32
|
+
"launchDevel": "export OTDEBUG=true && npm run launchKSApi"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"main": "./dist/index.cjs",
|
|
39
|
+
"module": "./src/index.js",
|
|
40
|
+
"exports": {
|
|
41
|
+
"require": "./dist/index.cjs",
|
|
42
|
+
"import": "./src/index.js",
|
|
43
|
+
"default": "./src/index.js"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@opentelemetry/instrumentation-http": "^0.51.0",
|
|
47
|
+
"@opentelemetry/resources": "^1.24.0",
|
|
48
|
+
"@opentelemetry/sdk-node": "^0.49.1",
|
|
49
|
+
"axios": "^1.6.8",
|
|
50
|
+
"dynamic-installer": "^1.0.1",
|
|
51
|
+
"express": "^4.19.2",
|
|
52
|
+
"import-from-string": "^0.0.4",
|
|
53
|
+
"js-yaml": "^4.1.0",
|
|
54
|
+
"nedb": "^1.8.0",
|
|
55
|
+
"readline": "^1.3.0",
|
|
56
|
+
"v8": "^0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@babel/cli": "^7.24.1",
|
|
60
|
+
"@babel/core": "^7.24.4",
|
|
61
|
+
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
62
|
+
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
|
63
|
+
"@babel/preset-env": "^7.24.4",
|
|
64
|
+
"@opentelemetry/api": "^1.8.0",
|
|
65
|
+
"@opentelemetry/auto-instrumentations-node": "^0.43.0",
|
|
66
|
+
"apipecker": "^1.3.1",
|
|
67
|
+
"babel-plugin-add-module-exports": "^1.0.4",
|
|
68
|
+
"babel-plugin-module-extension": "^0.1.3",
|
|
69
|
+
"nodemon": "^3.1.0"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,175 +1,176 @@
|
|
|
1
|
-
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
-
|
|
3
|
-
let dbglog = ()=>{};
|
|
4
|
-
|
|
5
|
-
if(process.env.OTDEBUG == "true")
|
|
6
|
-
dbglog = console.log;
|
|
7
|
-
|
|
8
|
-
//import in memory database
|
|
9
|
-
import dataStore from 'nedb';
|
|
10
|
-
|
|
11
|
-
export class InMemoryExporter {
|
|
12
|
-
constructor() {
|
|
13
|
-
this._spans = new dataStore();
|
|
14
|
-
this._stopped = true;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
static plugins = [];
|
|
18
|
-
|
|
19
|
-
export(readableSpans, resultCallback) {
|
|
20
|
-
try {
|
|
21
|
-
if (!this._stopped) {
|
|
22
|
-
// Prepare spans to be inserted into the in-memory database (remove circular references and convert to nested objects)
|
|
23
|
-
const cleanSpans = readableSpans
|
|
24
|
-
.map(nestedSpan => removeCircularRefs(nestedSpan))// to avoid JSON parsing error
|
|
25
|
-
.map(span => applyNesting(span))// to avoid dot notation in keys (neDB does not support dot notation in keys)
|
|
26
|
-
.filter(span => !span.attributes?.http?.target?.includes("/telemetry"));// to avoid telemetry spans
|
|
27
|
-
|
|
28
|
-
// Insert spans into the in-memory database
|
|
29
|
-
this._spans.insert(cleanSpans, (err, newDoc) => {
|
|
30
|
-
InMemoryExporter.plugins.forEach((p,i)=>{
|
|
31
|
-
cleanSpans.forEach((t)=>{
|
|
32
|
-
dbglog(`Sending trace <${t._id}> to plugin (Plugin #${i}) <${p.name}>`);
|
|
33
|
-
dbglog(`Trace: \n<${JSON.stringify(t,null,2)}`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* @
|
|
114
|
-
* @
|
|
115
|
-
*
|
|
116
|
-
* //
|
|
117
|
-
* //
|
|
118
|
-
* // "http.
|
|
119
|
-
* // "
|
|
120
|
-
* //
|
|
121
|
-
* //
|
|
122
|
-
* //
|
|
123
|
-
* //
|
|
124
|
-
* //
|
|
125
|
-
* // "
|
|
126
|
-
* //
|
|
127
|
-
* //
|
|
128
|
-
* //
|
|
129
|
-
* //
|
|
130
|
-
* //
|
|
131
|
-
* //
|
|
132
|
-
* //
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* @
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
|
|
3
|
+
let dbglog = ()=>{};
|
|
4
|
+
|
|
5
|
+
if(process.env.OTDEBUG == "true")
|
|
6
|
+
dbglog = console.log;
|
|
7
|
+
|
|
8
|
+
//import in memory database
|
|
9
|
+
import dataStore from 'nedb';
|
|
10
|
+
|
|
11
|
+
export class InMemoryExporter {
|
|
12
|
+
constructor() {
|
|
13
|
+
this._spans = new dataStore();
|
|
14
|
+
this._stopped = true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
static plugins = [];
|
|
18
|
+
|
|
19
|
+
export(readableSpans, resultCallback) {
|
|
20
|
+
try {
|
|
21
|
+
if (!this._stopped) {
|
|
22
|
+
// Prepare spans to be inserted into the in-memory database (remove circular references and convert to nested objects)
|
|
23
|
+
const cleanSpans = readableSpans
|
|
24
|
+
.map(nestedSpan => removeCircularRefs(nestedSpan))// to avoid JSON parsing error
|
|
25
|
+
.map(span => applyNesting(span))// to avoid dot notation in keys (neDB does not support dot notation in keys)
|
|
26
|
+
.filter(span => !span.attributes?.http?.target?.includes("/telemetry"));// to avoid telemetry spans
|
|
27
|
+
|
|
28
|
+
// Insert spans into the in-memory database
|
|
29
|
+
this._spans.insert(cleanSpans, (err, newDoc) => {
|
|
30
|
+
InMemoryExporter.plugins.forEach((p,i)=>{
|
|
31
|
+
cleanSpans.forEach((t)=>{
|
|
32
|
+
dbglog(`Sending trace <${t._id}> to plugin (Plugin #${i}) <${p.name}>`);
|
|
33
|
+
dbglog(`Trace: \n<${JSON.stringify(t,null,2)}`);
|
|
34
|
+
//TODO: This should be called newSpan instead of newTrace
|
|
35
|
+
p.newTrace(t);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
if (err) {
|
|
39
|
+
console.error(err);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error exporting spans\n' + error.message + '\n' + error.stack);
|
|
48
|
+
return resultCallback({
|
|
49
|
+
code: ExportResultCode.FAILED,
|
|
50
|
+
error: new Error('Error exporting spans\n' + error.message + '\n' + error.stack),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
start() {
|
|
55
|
+
this._stopped = false;
|
|
56
|
+
};
|
|
57
|
+
stop() {
|
|
58
|
+
this._stopped = true;
|
|
59
|
+
};
|
|
60
|
+
shutdown() {
|
|
61
|
+
this._stopped = true;
|
|
62
|
+
this._spans = new dataStore();
|
|
63
|
+
return this.forceFlush();
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Exports any pending spans in the exporter
|
|
67
|
+
*/
|
|
68
|
+
forceFlush() {
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
};
|
|
71
|
+
reset() {
|
|
72
|
+
this._spans = new dataStore();
|
|
73
|
+
};
|
|
74
|
+
getFinishedSpans() {
|
|
75
|
+
return this._spans;
|
|
76
|
+
};
|
|
77
|
+
activatePlugin(plugin){
|
|
78
|
+
dbglog(`Activating plugin <${plugin.getName()}>...`);
|
|
79
|
+
InMemoryExporter.plugins.push(plugin);
|
|
80
|
+
dbglog(`Plugin <${plugin.getName()}> active (Total active plugins: ${InMemoryExporter.plugins.length})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeCircularRefs(obj) {
|
|
85
|
+
const seen = new WeakMap(); // Used to keep track of visited objects
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// Replacer function to handle circular references
|
|
89
|
+
function replacer(key, value) {
|
|
90
|
+
if (key === "_spanProcessor") {
|
|
91
|
+
return "oas-telemetry skips this field to avoid circular reference";
|
|
92
|
+
}
|
|
93
|
+
// GENERIC CIRCULAR REFERENCE HANDLING
|
|
94
|
+
// if (typeof value === "object" && value !== null) {
|
|
95
|
+
// // If the object has been visited before, return the name prefixed with "CIRCULAR+"
|
|
96
|
+
// if (seen.has(value)) {
|
|
97
|
+
// return `CIRCULAR${key}`;
|
|
98
|
+
// }
|
|
99
|
+
// seen.set(value, key); // Mark the object as visited with its name
|
|
100
|
+
// }
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Convert the object to a string and then parse it back
|
|
105
|
+
// This will trigger the replacer function to handle circular references
|
|
106
|
+
const jsonString = JSON.stringify(obj, replacer);
|
|
107
|
+
return JSON.parse(jsonString);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Recursively converts dot-separated keys in an object to nested objects.
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} obj - The object to process.
|
|
114
|
+
* @returns {Object} - The object with all dot-separated keys converted to nested objects.
|
|
115
|
+
* @example
|
|
116
|
+
* // Input:
|
|
117
|
+
* // {
|
|
118
|
+
* // "http.method": "GET",
|
|
119
|
+
* // "http.url": "http://example.com",
|
|
120
|
+
* // "nested.obj.key": "value"
|
|
121
|
+
* // }
|
|
122
|
+
* // Output:
|
|
123
|
+
* // {
|
|
124
|
+
* // "http": {
|
|
125
|
+
* // "method": "GET",
|
|
126
|
+
* // "url": "http://example.com"
|
|
127
|
+
* // },
|
|
128
|
+
* // "nested": {
|
|
129
|
+
* // "obj": {
|
|
130
|
+
* // "key": "value"
|
|
131
|
+
* // }
|
|
132
|
+
* // }
|
|
133
|
+
* // }
|
|
134
|
+
*/
|
|
135
|
+
function convertToNestedObject(obj) {
|
|
136
|
+
const result = {};
|
|
137
|
+
|
|
138
|
+
for (const key in obj) {
|
|
139
|
+
const keys = key.split('.');
|
|
140
|
+
let temp = result;
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < keys.length; i++) {
|
|
143
|
+
const currentKey = keys[i];
|
|
144
|
+
|
|
145
|
+
if (i === keys.length - 1) {
|
|
146
|
+
// Last key, set the value
|
|
147
|
+
temp[currentKey] = obj[key];
|
|
148
|
+
} else {
|
|
149
|
+
// Intermediate key, ensure the object exists
|
|
150
|
+
if (!temp[currentKey]) {
|
|
151
|
+
temp[currentKey] = {};
|
|
152
|
+
}
|
|
153
|
+
temp = temp[currentKey];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Applies nesting to all dot-separated keys within an object.
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} obj - The object to apply nesting to.
|
|
165
|
+
* @returns {Object} - The transformed object with nested structures.
|
|
166
|
+
*/
|
|
167
|
+
function applyNesting(obj) {
|
|
168
|
+
// Recursively apply convertToNestedObject to each level of the object
|
|
169
|
+
for (const key in obj) {
|
|
170
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
171
|
+
obj[key] = applyNesting(obj[key]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return convertToNestedObject(obj);
|
|
175
176
|
}
|