@adobe/acc-js-sdk 1.1.4 → 1.1.7

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/src/client.js CHANGED
@@ -10,14 +10,14 @@ OF ANY KIND, either express or implied. See the License for the specific languag
10
10
  governing permissions and limitations under the License.
11
11
  */
12
12
  (function() {
13
- "use strict";
14
-
13
+ "use strict";
14
+
15
15
 
16
16
  /**********************************************************************************
17
- *
17
+ *
18
18
  * ACC JavaScript SDK
19
19
  * See README.md for usage
20
- *
20
+ *
21
21
  *********************************************************************************/
22
22
 
23
23
  /**
@@ -31,6 +31,7 @@ const Cipher = require('./crypto.js').Cipher;
31
31
  const DomUtil = require('./domUtil.js').DomUtil;
32
32
  const MethodCache = require('./methodCache.js').MethodCache;
33
33
  const OptionCache = require('./optionCache.js').OptionCache;
34
+ const CacheRefresher = require('./cacheRefresher.js').CacheRefresher;
34
35
  const request = require('./transport.js').request;
35
36
  const Application = require('./application.js').Application;
36
37
  const EntityAccessor = require('./entityAccessor.js').EntityAccessor;
@@ -38,16 +39,16 @@ const { Util } = require('./util.js');
38
39
 
39
40
  /**
40
41
  * @namespace Campaign
41
- *
42
+ *
42
43
  * @typedef {Object} SessionInfo
43
44
  * @memberOf Campaign
44
- *
45
+ *
45
46
  * @typedef {Object} RedirStatus
46
47
  * @memberOf Campaign
47
- *
48
+ *
48
49
  * @typedef {Object} PingStatus
49
50
  * @memberOf Campaign
50
- *
51
+ *
51
52
  * @typedef {Object} McPingStatus
52
53
  * @memberOf Campaign
53
54
  *
@@ -58,14 +59,14 @@ const { Util } = require('./util.js');
58
59
 
59
60
  /**
60
61
  * Java Script Proxy handler for an XTK object. An XTK object is one constructed with the following syntax:
61
- *
62
+ *
62
63
  * <code>
63
64
  * NLWS.xtkQueryDef.create(...)
64
65
  * </code>
65
- *
66
+ *
66
67
  * Any Xtk methods can be called directly on such an object using this proxy handler which will lookup
67
68
  * the method definition and manage parameters marshalling and SOAP call
68
- *
69
+ *
69
70
  * @private
70
71
  * @memberof Campaign
71
72
  */
@@ -83,20 +84,20 @@ const xtkObjectHandler = {
83
84
  return new Proxy(caller, {
84
85
  apply: function(target, thisArg, argumentsList) {
85
86
  return target(thisArg, argumentsList);
86
- }
87
+ }
87
88
  });
88
89
 
89
90
  }
90
91
  };
91
92
 
92
93
  /**
93
- * Java Script Proxy handler for NLWS.
94
+ * Java Script Proxy handler for NLWS.
94
95
  * The proxy resolves constructs such as
95
- *
96
+ *
96
97
  * <code>
97
98
  * result = await client.NLWS.xtkSession.getServerTime();
98
99
  * </code>
99
- *
100
+ *
100
101
  * To get a handler, call the `clientHandler` function and optionally pass a representation.
101
102
  * If no representation is passed (undefined), the representation set at the client level
102
103
  * will be used, which is the default behavior.
@@ -194,7 +195,7 @@ const clientHandler = (representation, headers, pushDownOptions) => {
194
195
  return new Proxy(caller, {
195
196
  apply: function(target, thisArg, argumentsList) {
196
197
  return target(thisArg, argumentsList);
197
- }
198
+ }
198
199
  });
199
200
  }
200
201
  });
@@ -206,7 +207,7 @@ const clientHandler = (representation, headers, pushDownOptions) => {
206
207
  // Campaign credentials
207
208
  // ========================================================================================
208
209
 
209
- /**
210
+ /**
210
211
  * @class
211
212
  * @constructor
212
213
  * @private
@@ -222,7 +223,7 @@ class Credentials {
222
223
  * @param {string} securityToken the security token. Will use an empty token if not specified
223
224
  */
224
225
  constructor(type, sessionToken, securityToken) {
225
- if (type != "UserPassword" && type != "ImsServiceToken" && type != "SessionToken" &&
226
+ if (type != "UserPassword" && type != "ImsServiceToken" && type != "SessionToken" &&
226
227
  type != "AnonymousUser" && type != "SecurityToken" && type != "BearerToken")
227
228
  throw CampaignException.INVALID_CREDENTIALS_TYPE(type);
228
229
  this._type = type;
@@ -236,7 +237,7 @@ class Credentials {
236
237
 
237
238
  /**
238
239
  * For "UserPassword" type credentials, return the user name
239
- *
240
+ *
240
241
  * @private
241
242
  * @returns {string} the user name
242
243
  */
@@ -250,7 +251,7 @@ class Credentials {
250
251
 
251
252
  /**
252
253
  * For "UserPassword" type credentials, return the user password
253
- *
254
+ *
254
255
  * @private
255
256
  * @returns {string} the user password
256
257
  */
@@ -288,7 +289,7 @@ class Credentials {
288
289
  * @property {number} timeout - Can be set to change the HTTP call timeout. Value is passed in ms.
289
290
  * @memberOf Campaign
290
291
  */
291
-
292
+
292
293
 
293
294
  /**
294
295
  * @class
@@ -330,7 +331,7 @@ class ConnectionParameters {
330
331
 
331
332
  this._options.entityCacheTTL = options.entityCacheTTL || 1000*300; // 5 mins
332
333
  this._options.methodCacheTTL = options.methodCacheTTL || 1000*300; // 5 mins
333
- this._options.optionCacheTTL = options.optionCacheTTL || 1000*300; // 5 mins
334
+ this._options.optionCacheTTL = options.optionCacheTTL || 1000*300; // 5 mins
334
335
  this._options.traceAPICalls = options.traceAPICalls === null || options.traceAPICalls ? !!options.traceAPICalls : false;
335
336
  this._options.transport = options.transport || request;
336
337
 
@@ -342,10 +343,10 @@ class ConnectionParameters {
342
343
  storage = options.storage;
343
344
  try {
344
345
  if (!storage)
345
- storage = localStorage;
346
+ storage = localStorage;
346
347
  } catch (ex) {
347
348
  /* ignore error if localStorage not found */
348
- }
349
+ }
349
350
  }
350
351
  this._options._storage = storage;
351
352
  this._options.refreshClient = options.refreshClient;
@@ -361,7 +362,7 @@ class ConnectionParameters {
361
362
 
362
363
  /**
363
364
  * Creates connection parameters for a Campaign instance, using a user name and password
364
- *
365
+ *
365
366
  * @param {string} endpoint The campaign endpoint (URL)
366
367
  * @param {string} user The user name
367
368
  * @param {string} password The user password
@@ -375,7 +376,7 @@ class ConnectionParameters {
375
376
 
376
377
  /**
377
378
  * Creates connection parameters for a Campaign instance from bearer token
378
- *
379
+ *
379
380
  * @param {string} endpoint The campaign endpoint (URL)
380
381
  * @param {string} bearerToken IMS bearer token
381
382
  * @param {*} options connection options
@@ -387,7 +388,7 @@ class ConnectionParameters {
387
388
  }
388
389
  /**
389
390
  * Creates connection parameters for a Campaign instance, using an IMS service token and a user name (the user to impersonate)
390
- *
391
+ *
391
392
  * @param {string} endpoint The campaign endpoint (URL)
392
393
  * @param {string} user The user name
393
394
  * @param {string} serviceToken The IMS service token
@@ -401,7 +402,7 @@ class ConnectionParameters {
401
402
 
402
403
  /**
403
404
  * Creates connection parameters for a Campaign instance, using a session token
404
- *
405
+ *
405
406
  * @static
406
407
  * @param {string} endpoint The campaign endpoint (URL)
407
408
  * @param {string} sessionToken The session token
@@ -418,7 +419,7 @@ class ConnectionParameters {
418
419
  * Typically, called when embedding the SDK in Campaign: the session token will be
419
420
  * passed automatically as a cookie, so only the security token is actually needed
420
421
  * to logon
421
- *
422
+ *
422
423
  * @static
423
424
  * @param {string} endpoint The campaign endpoint (URL)
424
425
  * @param {string} securityToken The session token
@@ -432,7 +433,7 @@ class ConnectionParameters {
432
433
 
433
434
  /**
434
435
  * Creates connection parameters for a Campaign instance for an anonymous user
435
- *
436
+ *
436
437
  * @param {string} endpoint The campaign endpoint (URL)
437
438
  * @param {Campaign.ConnectionOptions} options connection options
438
439
  * @returns {ConnectionParameters} a ConnectionParameters object which can be used to create a Client
@@ -447,7 +448,7 @@ class ConnectionParameters {
447
448
  * Creates connection parameters for a Campaign instance, using an external account. This can be used to connect
448
449
  * to a mid-sourcing instance, or to a message center instance. This function will lookup the external account,
449
450
  * and use its credentials to get connection parameters to the corresponding Campaign instance
450
- *
451
+ *
451
452
  * @param {Client} client The Campaign Client from which to lookup the external account (normally, a connected client to the marketing instance)
452
453
  * @param {string} extAccountName The name of the external account. Only mid-sourcing accounts (type 3) are supported
453
454
  * @returns {ConnectionParameters} a ConnectionParameters object which can be used to create a Client
@@ -493,6 +494,102 @@ class ConnectionParameters {
493
494
 
494
495
  }
495
496
 
497
+ // ========================================================================================
498
+ // File Uploader
499
+ // ========================================================================================
500
+
501
+ /**
502
+ * File Uploader API for JS SDK(Currently available only in browsers)
503
+ * @private
504
+ * @ignore
505
+ * @memberof Campaign
506
+ * @param client
507
+ * @returns {{upload: (function(*=): Promise<{name: string, md5: string, type: string, size: string, url: string}>)}}
508
+ */
509
+ const fileUploader = (client) => {
510
+
511
+ return {
512
+ /**
513
+ * This is the exposed/public method for fileUploader instance which will do all the processing related to the upload process internally and returns the promise containing all the required data.
514
+ * @ignore
515
+ * @param file, where file is an instance of [File](https://developer.mozilla.org/en-US/docs/Web/API/File)
516
+ * @returns {Promise<{name: string, md5: string, type: string, size: string, url: string}>}
517
+ */
518
+ upload: (file) => {
519
+ console.log(`fileuploader.upload is an experimental feature and is not currently fully functional. It is work in progress and will change in the future.`);
520
+ return new Promise((resolve, reject) => {
521
+ try {
522
+ if (!Util.isBrowser()) {
523
+ throw 'File uploading is only supported in browser based calls.';
524
+ }
525
+ const data = new FormData();
526
+ data.append('file_noMd5', file);
527
+ //TODO: Needs to be refactored after cookie issue get resolved.
528
+ client._makeHttpCall({
529
+ url: `${client._connectionParameters._endpoint}/nl/jsp/uploadFile.jsp`,
530
+ processData: false,
531
+ credentials: 'include',
532
+ method: 'POST',
533
+ body: data,
534
+ headers: {
535
+ 'x-security-token': client._securityToken,
536
+ 'Cookie': '__sessiontoken=' + client._sessionToken,
537
+ }
538
+ }).then((okay) => {
539
+ if (!okay.startsWith('Ok')) {
540
+ throw okay;
541
+ }
542
+ const iframe = document.createElement('iframe');
543
+ iframe.style.height = 0;
544
+ iframe.style.width = 0;
545
+ document.controller = { // Written to support https://git.corp.adobe.com/Campaign/ac/blob/v6-master/nl/datakit/nl/jsp/uploadFile.jsp
546
+ uploadFileCallBack: async (data) => {
547
+ if (!data || data.length !== 1) {
548
+ // Tried to replicate the logic for file upload functionality written here:
549
+ // https://git.corp.adobe.com/Campaign/ac/blob/v6-master/wpp/xtk/web/dce/uploader.js
550
+ return reject(CampaignException.FILE_UPLOAD_FAILED(file.name, 'Malformed data:' + data.toString()));
551
+ }
552
+ const counter = await client.NLWS.xtkCounter.increaseValue({name: 'xtkResource'});
553
+ const fileRes= {
554
+ internalName: 'RES' + counter,
555
+ md5: data[0].md5,
556
+ label: data[0].fileName,
557
+ fileName: data[0].fileName,
558
+ originalName: data[0].fileName,
559
+ useMd5AsFilename: '1',
560
+ storageType: 5,
561
+ xtkschema: 'xtk:fileRes'
562
+
563
+ };
564
+ await client.NLWS.xtkSession.write(fileRes);
565
+ await client.NLWS.xtkFileRes.create(fileRes).publishIfNeeded();
566
+ const url = await client.NLWS.xtkFileRes.create(fileRes).getURL();
567
+ resolve({
568
+ name: data[0].fileName,
569
+ md5: data[0].md5,
570
+ type: file.type,
571
+ size: file.size,
572
+ url: url
573
+ });
574
+ }
575
+ };
576
+ const html = `<body>${okay}</body>`;
577
+ document.body.appendChild(iframe);
578
+ iframe.contentWindow.document.open();
579
+ iframe.contentWindow.document.write(html);
580
+ iframe.contentWindow.document.close();
581
+ }).catch((ex) => {
582
+ reject(CampaignException.FILE_UPLOAD_FAILED(file.name, ex));
583
+ });
584
+ } catch (ex) {
585
+ reject(CampaignException.FILE_UPLOAD_FAILED(file.name, ex));
586
+ }
587
+ });
588
+ }
589
+ };
590
+ };
591
+
592
+
496
593
  // ========================================================================================
497
594
  // ACC Client
498
595
  // ========================================================================================
@@ -508,7 +605,7 @@ class Client {
508
605
  /**
509
606
  * ACC API Client.
510
607
  * Do not create directly, use SDK.init instead
511
- *
608
+ *
512
609
  * @param {Campaign.SDK} sdk is the global sdk object used to create the client
513
610
  * @param {Campaign.ConnectionParameters} user user name, for instance admin
514
611
  */
@@ -521,9 +618,9 @@ class Client {
521
618
  this._sessionToken = undefined;
522
619
  this._securityToken = undefined;
523
620
  this._installedPackages = {}; // package set (key and value = package id, ex: "nms:amp")
524
-
621
+
525
622
  this._secretKeyCipher = undefined;
526
-
623
+
527
624
  this._storage = connectionParameters._options._storage;
528
625
  // TODO late cache initiallzation because need XtkDatabaseId / instance name
529
626
  var instanceKey = connectionParameters._endpoint || "";
@@ -532,23 +629,31 @@ class Client {
532
629
  const rootKey = `acc.js.sdk.${sdk.getSDKVersion().version}.${instanceKey}.cache`;
533
630
 
534
631
  this._entityCache = new XtkEntityCache(this._storage, `${rootKey}.XtkEntityCache`, connectionParameters._options.entityCacheTTL);
632
+ this._entityCacheRefresher = new CacheRefresher(this._entityCache, this, "xtk:schema", `${rootKey}.XtkEntityCache`);
535
633
  this._methodCache = new MethodCache(this._storage, `${rootKey}.MethodCache`, connectionParameters._options.methodCacheTTL);
536
634
  this._optionCache = new OptionCache(this._storage, `${rootKey}.OptionCache`, connectionParameters._options.optionCacheTTL);
635
+ this._optionCacheRefresher = new CacheRefresher(this._optionCache, this, "xtk:option", `${rootKey}.OptionCache`);
537
636
  this.NLWS = new Proxy(this, clientHandler());
538
637
 
539
638
  this._transport = connectionParameters._options.transport;
540
639
  this._traceAPICalls = connectionParameters._options.traceAPICalls;
541
640
  this._observers = [];
641
+ this._cacheChangeListeners = [];
542
642
  this._refreshClient = connectionParameters._options.refreshClient;
543
643
 
544
644
  // expose utilities
645
+ /**
646
+ * File Uploader API
647
+ * @type {{upload: (function(*=): Promise<{name: string, md5: string, type: string, size: string, url: string}>)}}
648
+ */
649
+ this.fileUploader = fileUploader(this);
545
650
 
546
- /**
651
+ /**
547
652
  * Accessor to DOM helpers
548
653
  * @type {XML.DomUtil}
549
654
  */
550
655
  this.DomUtil = DomUtil;
551
- /**
656
+ /**
552
657
  * Accessor to a XtkCaster
553
658
  * @type {XtkCaster}
554
659
  */
@@ -558,12 +663,27 @@ class Client {
558
663
  * @type {Campaign.Application}
559
664
  */
560
665
  this.application = null;
666
+
667
+ // Context for observability. See logon() function which will fill this context
668
+ this._lastStatsReport = Date.now();
669
+ this._observabilityContext = {
670
+ eventId: 0,
671
+ client: {
672
+ sdkVersion: this.sdk.getSDKVersion().version,
673
+ endpoint: this._connectionParameters._endpoint,
674
+ createdAt: Date.now(),
675
+ clientApp: this._connectionParameters._options.clientApp,
676
+ }
677
+ };
678
+ if (this._connectionParameters._credentials) {
679
+ this._observabilityContext.client.type = this._connectionParameters._credentials.type;
680
+ }
561
681
  }
562
682
 
563
683
  /**
564
684
  * Override the transport. By default, we are using axios, but this can be customised.
565
685
  * See transport.js and documentation in the README for more details
566
- * @param {*} transport
686
+ * @param {*} transport
567
687
  */
568
688
  setTransport(transport) {
569
689
  this._transport = transport;
@@ -571,7 +691,7 @@ class Client {
571
691
 
572
692
  /**
573
693
  * Get the user agent string to use in all HTTP requests
574
- *
694
+ *
575
695
  * @returns {string} the user agent string
576
696
  */
577
697
  _getUserAgentString() {
@@ -581,7 +701,7 @@ class Client {
581
701
 
582
702
  /**
583
703
  * Convert an XML object into a representation
584
- *
704
+ *
585
705
  * @private
586
706
  * @param {DOMElement} xml the XML DOM element to convert
587
707
  * @param {string} representation the expected representation ('xml', 'BadgerFish', or 'SimpleJson'). If not set, will use the current representation
@@ -598,7 +718,7 @@ class Client {
598
718
 
599
719
  /**
600
720
  * Convert to an XML object from a representation
601
- *
721
+ *
602
722
  * @private
603
723
  * @param {string} rootName the name of the root XML element
604
724
  * @param {XML.XtkObject} entity the object to convert
@@ -618,7 +738,7 @@ class Client {
618
738
 
619
739
  /**
620
740
  * Convert between 2 representations
621
- *
741
+ *
622
742
  * @private
623
743
  * @param {XML.XtkObject} entity the object to convert
624
744
  * @param {string} fromRepresentation the source representation ('xml', 'BadgerFish', or 'SimpleJson').
@@ -636,7 +756,7 @@ class Client {
636
756
 
637
757
  /**
638
758
  * Compare two representations
639
- *
759
+ *
640
760
  * @private
641
761
  * @param {string} rep1 the first representation ('xml', 'BadgerFish', or 'SimpleJson')
642
762
  * @param {string} rep2 the second representation ('xml', 'BadgerFish', or 'SimpleJson')
@@ -652,7 +772,7 @@ class Client {
652
772
 
653
773
  /**
654
774
  * Activate / deactivate tracing of API calls
655
- *
775
+ *
656
776
  * @param {boolean} trace indicates whether to activate tracing or not
657
777
  */
658
778
  traceAPICalls(trace) {
@@ -688,9 +808,67 @@ class Client {
688
808
  this._observers.map((observer) => callback(observer));
689
809
  }
690
810
 
811
+ _trackEvent(eventName, parentEvent, payload) {
812
+ try {
813
+ if (payload && payload.name === 'CampaignException') {
814
+ payload = {
815
+ detail: payload.detail,
816
+ errorCode: payload.errorCode,
817
+ faultCode: payload.faultCode,
818
+ faultString: payload.faultString,
819
+ message: payload.message,
820
+ statusCode: payload.statusCode,
821
+ };
822
+ }
823
+ this._observabilityContext.eventId = this._observabilityContext.eventId + 1;
824
+ const now = Date.now();
825
+ const event = {
826
+ client: this._observabilityContext.client,
827
+ session: this._observabilityContext.session,
828
+ eventId: this._observabilityContext.eventId,
829
+ eventName: eventName,
830
+ payload: payload,
831
+ timestamp: now,
832
+ };
833
+ if (parentEvent) event.parentEventId = parentEvent.eventId;
834
+ this._notifyObservers((observer) => observer.event && observer.event(event, parentEvent));
835
+
836
+ // Regularly report internal stats every 5 mins
837
+ if ((now - this._lastStatsReport) >= 300000) {
838
+ this._lastStatsReport = now;
839
+ this._trackInternalStats();
840
+ }
841
+
842
+ return event;
843
+ } catch (error) {
844
+ console.info(`Failed to track observability event`, error);
845
+ }
846
+ }
847
+
848
+ _trackInternalStats() {
849
+ this._trackCacheStats('entityCache', this._entityCache);
850
+ this._trackCacheStats('optionCache', this._optionCache);
851
+ this._trackCacheStats('methodCache', this._methodCache);
852
+ }
853
+
854
+ _trackCacheStats(name, cache) {
855
+ if (!cache || !cache._stats) return;
856
+ this._trackEvent('CACHE//stats', undefined, {
857
+ name: name,
858
+ reads: cache._stats.reads,
859
+ writes: cache._stats.writes,
860
+ removals: cache._stats.removals,
861
+ clears: cache._stats.clears,
862
+ memoryHits: cache._stats.memoryHits,
863
+ storageHits: cache._stats.storageHits,
864
+ loads: cache._stats.loads,
865
+ saves: cache._stats.saves,
866
+ });
867
+ }
868
+
691
869
  /**
692
870
  * Is the client logged?
693
- *
871
+ *
694
872
  * @returns {boolean} a boolean indicating if the client is logged or not
695
873
  */
696
874
  isLogged() {
@@ -725,7 +903,7 @@ class Client {
725
903
 
726
904
  /**
727
905
  * Prepares a SOAP call, including authentication, headers...
728
- *
906
+ *
729
907
  * @private
730
908
  * @param {string} urn is the API name space, usually the schema. For instance xtk:session
731
909
  * @param {string} method is the method to call, for instance Logon
@@ -734,8 +912,8 @@ class Client {
734
912
  * parameters should be set
735
913
  */
736
914
  _prepareSoapCall(urn, method, internal, extraHttpHeaders, pushDownOptions) {
737
- const soapCall = new SoapMethodCall(this._transport, urn, method,
738
- this._sessionToken, this._securityToken,
915
+ const soapCall = new SoapMethodCall(this._transport, urn, method,
916
+ this._sessionToken, this._securityToken,
739
917
  this._getUserAgentString(),
740
918
  Object.assign({}, this._connectionParameters._options, pushDownOptions),
741
919
  extraHttpHeaders);
@@ -753,16 +931,30 @@ class Client {
753
931
  async _retrySoapCall(soapCall) {
754
932
  soapCall.retry = false;
755
933
  soapCall._retryCount = soapCall._retryCount + 1;
756
- var newClient = await this._refreshClient(this);
934
+ var newClient = await this._refreshClient(this);
757
935
  soapCall.finalize(newClient._soapEndPoint(), newClient);
936
+ const safeCallData = Util.trim(soapCall.request.data);
758
937
  if (this._traceAPICalls) {
759
- const safeCallData = Util.trim(soapCall.request.data);
760
938
  console.log(`RETRY SOAP//request ${safeCallData}`);
761
939
  }
762
- await soapCall.execute();
763
- if (this._traceAPICalls) {
940
+ const event = this._trackEvent('SOAP//request', undefined, {
941
+ urn: soapCall.urn,
942
+ methodName: soapCall.methodName,
943
+ internal: soapCall.internal,
944
+ retry: true,
945
+ retryCount: soapCall._retryCount,
946
+ safeCallData: safeCallData,
947
+ });
948
+ try {
949
+ await soapCall.execute();
764
950
  const safeCallResponse = Util.trim(soapCall.response);
765
- console.log(`SOAP//response ${safeCallResponse}`);
951
+ if (this._traceAPICalls) {
952
+ console.log(`SOAP//response ${safeCallResponse}`);
953
+ }
954
+ this._trackEvent('SOAP//response', event, { safeCallResponse: safeCallResponse });
955
+ } catch(error) {
956
+ this._trackEvent('SOAP//failure', event, error);
957
+ throw error;
766
958
  }
767
959
  return;
768
960
  }
@@ -779,7 +971,7 @@ class Client {
779
971
  /**
780
972
  * After a SOAP method call has been prepared with '_prepareSoapCall', and parameters have been added,
781
973
  * this function actually executes the SOAP call
782
- *
974
+ *
783
975
  * @private
784
976
  * @param {SOAP.SoapMethodCall} soapCall the SOAP method to call
785
977
  */
@@ -788,24 +980,34 @@ class Client {
788
980
  if (soapCall.requiresLogon() && !that.isLogged())
789
981
  throw CampaignException.NOT_LOGGED_IN(soapCall, `Cannot execute SOAP call ${soapCall.urn}#${soapCall.methodName}: you are not logged in. Use the Logon function first`);
790
982
  soapCall.finalize(this._soapEndPoint());
791
-
983
+
792
984
  const safeCallData = Util.trim(soapCall.request.data);
793
985
  if (that._traceAPICalls)
794
986
  console.log(`SOAP//request ${safeCallData}`);
795
987
  that._notifyObservers((observer) => observer.onSOAPCall && observer.onSOAPCall(soapCall, safeCallData) );
796
-
988
+
989
+ const event = this._trackEvent('SOAP//request', undefined, {
990
+ urn: soapCall.urn,
991
+ methodName: soapCall.methodName,
992
+ internal: soapCall.internal,
993
+ retry: false,
994
+ retryCount: soapCall._retryCount,
995
+ safeCallData: safeCallData,
996
+ });
797
997
  return soapCall.execute()
798
998
  .then(() => {
799
999
  const safeCallResponse = Util.trim(soapCall.response);
800
1000
  if (that._traceAPICalls)
801
1001
  console.log(`SOAP//response ${safeCallResponse}`);
802
1002
  that._notifyObservers((observer) => observer.onSOAPCallSuccess && observer.onSOAPCallSuccess(soapCall, safeCallResponse) );
1003
+ this._trackEvent('SOAP//response', event, { safeCallResponse: safeCallResponse });
803
1004
  return Promise.resolve();
804
1005
  })
805
1006
  .catch((ex) => {
806
1007
  if (that._traceAPICalls)
807
1008
  console.log(`SOAP//failure ${ex.toString()}`);
808
1009
  that._notifyObservers((observer) => observer.onSOAPCallFailure && observer.onSOAPCallFailure(soapCall, ex) );
1010
+ this._trackEvent('SOAP//failure', event, ex);
809
1011
  // Call session expiration callback in case of 401
810
1012
  if (ex.statusCode == 401 && that._refreshClient && soapCall.retry) {
811
1013
  return this._retrySoapCall(soapCall);
@@ -815,6 +1017,32 @@ class Client {
815
1017
  });
816
1018
  }
817
1019
 
1020
+ _onLogon() {
1021
+ this.application = new Application(this);
1022
+ this.application._registerCacheChangeListener();
1023
+ this._observabilityContext.instance = {
1024
+ buildNumber: this.application.buildNumber,
1025
+ version: this.application.version,
1026
+ instanceName: this.application.instanceName,
1027
+ };
1028
+ if (this.application.operator) {
1029
+ this._observabilityContext.instance.operator = {
1030
+ login: this.application.operator.login,
1031
+ id: this.application.operator.id,
1032
+ timezone: this.application.operator.timezone,
1033
+ rights: this.application.operator.rights,
1034
+ packages: this.application.operator.packages,
1035
+ };
1036
+ }
1037
+ this._observabilityContext.session = {
1038
+ logonAt: Date.now(),
1039
+ };
1040
+ }
1041
+
1042
+ _onLogoff() {
1043
+ delete this._observabilityContext.instance;
1044
+ delete this._observabilityContext.session;
1045
+ }
818
1046
 
819
1047
  /**
820
1048
  * Login to an instance
@@ -839,6 +1067,8 @@ class Client {
839
1067
  // See NEO-35259
840
1068
  this._connectionParameters._options.extraHttpHeaders['X-Query-Source'] = `${version}${clientApp? "," + clientApp : ""}`;
841
1069
 
1070
+ this._trackEvent('SDK//logon', undefined, {});
1071
+
842
1072
  // Clear session token cookie to ensure we're not inheriting an expired cookie. See NEO-26589
843
1073
  if (credentials._type != "SecurityToken" && typeof document != "undefined") {
844
1074
  document.cookie = '__sessiontoken=;path=/;';
@@ -848,7 +1078,7 @@ class Client {
848
1078
  that._installedPackages = {};
849
1079
  that._sessionToken = credentials._sessionToken;
850
1080
  that._securityToken = "";
851
- that.application = new Application(that);
1081
+ that._onLogon();
852
1082
  return Promise.resolve();
853
1083
  }
854
1084
  else if (credentials._type == "SecurityToken") {
@@ -856,7 +1086,7 @@ class Client {
856
1086
  that._installedPackages = {};
857
1087
  that._sessionToken = "";
858
1088
  that._securityToken = credentials._securityToken;
859
- that.application = new Application(that);
1089
+ that._onLogon();
860
1090
  return Promise.resolve();
861
1091
  }
862
1092
  else if (credentials._type == "UserPassword" || credentials._type == "BearerToken") {
@@ -880,10 +1110,10 @@ class Client {
880
1110
  else {
881
1111
  const bearerToken = credentials._bearerToken;
882
1112
  soapCall.writeString("bearerToken", bearerToken);
883
- }
1113
+ }
884
1114
  return this._makeSoapCall(soapCall).then(function() {
885
1115
  const sessionToken = soapCall.getNextString();
886
-
1116
+
887
1117
  that._sessionInfo = soapCall.getNextDocument();
888
1118
  that._installedPackages = {};
889
1119
  const userInfo = DomUtil.findElement(that._sessionInfo, "userInfo");
@@ -895,7 +1125,7 @@ class Client {
895
1125
  that._installedPackages[name] = name;
896
1126
  pack = DomUtil.getNextSiblingElement(pack);
897
1127
  }
898
-
1128
+
899
1129
  const securityToken = soapCall.getNextString();
900
1130
  soapCall.checkNoMoreArgs();
901
1131
  // Sanity check: we should have both a session token and a security token.
@@ -907,7 +1137,7 @@ class Client {
907
1137
  that._sessionToken = sessionToken;
908
1138
  that._securityToken = securityToken;
909
1139
 
910
- that.application = new Application(that);
1140
+ that._onLogon();
911
1141
  });
912
1142
  }
913
1143
  else {
@@ -918,7 +1148,7 @@ class Client {
918
1148
 
919
1149
  /**
920
1150
  * Get details about the session (assumes client is logged)
921
- *
1151
+ *
922
1152
  * @param {string} representation the expected representation. If not set, will use the default client representation
923
1153
  * @returns {Campaign.SessionInfo} details about the session
924
1154
  */
@@ -932,47 +1162,55 @@ class Client {
932
1162
  */
933
1163
  logoff() {
934
1164
  var that = this;
935
- if (!that.isLogged()) return;
936
- const credentials = this._connectionParameters._credentials;
937
- if (credentials._type != "SessionToken" && credentials._type != "AnonymousUser") {
938
- var soapCall = that._prepareSoapCall("xtk:session", "Logoff", false, this._connectionParameters._options.extraHttpHeaders);
1165
+ try {
1166
+ if (!that.isLogged()) return;
1167
+ that.application._unregisterCacheChangeListener();
1168
+ that._unregisterAllCacheChangeListeners();
1169
+ this.stopRefreshCaches();
1170
+ const credentials = this._connectionParameters._credentials;
1171
+ if (credentials._type != "SessionToken" && credentials._type != "AnonymousUser") {
1172
+ var soapCall = that._prepareSoapCall("xtk:session", "Logoff", false, this._connectionParameters._options.extraHttpHeaders);
939
1173
  return this._makeSoapCall(soapCall).then(function() {
1174
+ that._sessionToken = "";
1175
+ that._securityToken = "";
1176
+ that.application = null;
1177
+ soapCall.checkNoMoreArgs();
1178
+ });
1179
+ }
1180
+ else {
940
1181
  that._sessionToken = "";
941
1182
  that._securityToken = "";
942
1183
  that.application = null;
943
- soapCall.checkNoMoreArgs();
944
- });
945
- }
946
- else {
947
- that._sessionToken = "";
948
- that._securityToken = "";
949
- that.application = null;
1184
+ }
1185
+ } finally {
1186
+ this._trackEvent('SDK//logoff', undefined, {});
1187
+ this._onLogoff();
950
1188
  }
951
1189
  }
952
1190
 
953
1191
  /**
954
1192
  * Get the value of an option
955
- *
1193
+ *
956
1194
  * @param {string} name is the option name, for instance XtkDatabaseId
957
1195
  * @param {boolean} useCache indicates whether to use the cache or not. Default is true
958
1196
  * @return the option value, casted in the expected data type. If the option does not exist, it will return null.
959
1197
  */
960
1198
  async getOption(name, useCache = true) {
961
- var value;
962
- if (useCache)
963
- value = this._optionCache.get(name);
964
- if (value === undefined) {
965
- const option = await this.NLWS.xtkSession.getOption(name);
966
- value = this._optionCache.put(name, option);
967
- this._optionCache.put(name, option);
968
- }
969
- return value;
1199
+ var value;
1200
+ if (useCache) {
1201
+ value = this._optionCache.get(name);
1202
+ }
1203
+ if (value === undefined) {
1204
+ const option = await this.NLWS.xtkSession.getOption(name);
1205
+ value = this._optionCache.put(name, option);
1206
+ }
1207
+ return value;
970
1208
  }
971
1209
 
972
1210
  /**
973
1211
  * Set an option value. Creates the option if it does not exists. Update the option
974
1212
  * if it exists already
975
- *
1213
+ *
976
1214
  * @param {string} name the option name
977
1215
  * @param {*} rawValue the value to set
978
1216
  * @param {string} description the optional description of the option
@@ -989,7 +1227,7 @@ class Client {
989
1227
  var attName = XtkCaster._variantStorageAttribute(type);
990
1228
  if (!attName) {
991
1229
  // could not infer the storage type of the attribute to use to store the value (when option did not exist before) => assume string
992
- type = 6;
1230
+ type = 6;
993
1231
  attName = "stringValue";
994
1232
  }
995
1233
  var doc = { xtkschema: "xtk:option", _operation: "insertOrUpdate", _key: "@name", name: name, dataType: type };
@@ -1032,9 +1270,53 @@ class Client {
1032
1270
  this.clearOptionCache();
1033
1271
  }
1034
1272
 
1273
+ /**
1274
+ * Start auto refresh of all caches
1275
+ * @param {integer} refreshFrequency refresh frequency in ms. 10000 ms by default.
1276
+ */
1277
+ startRefreshCaches(refreshFrequency) {
1278
+ if (refreshFrequency === undefined || refreshFrequency === null)
1279
+ refreshFrequency = 10000;
1280
+ this._optionCacheRefresher.startAutoRefresh(refreshFrequency);
1281
+ // Start auto refresh for entityCache a little later
1282
+ setTimeout(() => { this._entityCacheRefresher.startAutoRefresh(refreshFrequency); }, refreshFrequency/2);
1283
+ }
1284
+ /**
1285
+ * Stop auto refresh of all caches
1286
+ */
1287
+ stopRefreshCaches() {
1288
+ this._optionCacheRefresher.stopAutoRefresh();
1289
+ this._entityCacheRefresher.stopAutoRefresh();
1290
+ }
1291
+
1292
+ // Register a callback to be called when a schema has been modified and should be removed
1293
+ // from the caches
1294
+ _registerCacheChangeListener(listener) {
1295
+ this._cacheChangeListeners.push(listener);
1296
+ }
1297
+
1298
+ // Unregister a cache change listener
1299
+ _unregisterCacheChangeListener(listener) {
1300
+ for (var i = 0; i < this._cacheChangeListeners.length; i++) {
1301
+ if (this._cacheChangeListeners[i] == listener) {
1302
+ this._cacheChangeListeners.splice(i, 1);
1303
+ break;
1304
+ }
1305
+ }
1306
+ }
1307
+
1308
+ // Unregister all cache change listener
1309
+ _unregisterAllCacheChangeListeners() {
1310
+ this._cacheChangeListeners = [];
1311
+ }
1312
+
1313
+ _notifyCacheChangeListeners(schemaId) {
1314
+ this._cacheChangeListeners.map((listener) => listener.invalidateCacheItem(schemaId));
1315
+ }
1316
+
1035
1317
  /**
1036
1318
  * Tests if a package is installed
1037
- *
1319
+ *
1038
1320
  * @param {string} packageId the package identifier, for instance: "nms:amp"
1039
1321
  * @param {string} optionalName if set, the first parameter will be interpreted as the namespace (ex: "nms") and the second as the name, ex: "amp"
1040
1322
  * @returns {boolean} a boolean indicating if the package is installed or not
@@ -1049,8 +1331,8 @@ class Client {
1049
1331
 
1050
1332
  /**
1051
1333
  * Obtains a cipher that can be used to encrypt/decrypt passwords, using the database secret key.
1052
- * This is used for example for mid-sourcing account.
1053
- *
1334
+ * This is used for example for mid-sourcing account.
1335
+ *
1054
1336
  * @private
1055
1337
  * @deprecated since version 1.0.0
1056
1338
  */
@@ -1065,10 +1347,10 @@ class Client {
1065
1347
 
1066
1348
  /**
1067
1349
  * Gets an entity, such as a schema, a source schema, a form, a navtree, etc. Each Campaign entity has a MD5 which is used to determine
1068
- * if an entity has changed or not. The GetEntityIfMoreRecent SOAP call can use this MD5 to avoid returning entities that did not
1350
+ * if an entity has changed or not. The GetEntityIfMoreRecent SOAP call can use this MD5 to avoid returning entities that did not
1069
1351
  * actually changed. Currently, the SDK, however is not able to use the MD5 and will perform a SOAP call every time the function is
1070
1352
  * called and return the whole entity
1071
- *
1353
+ *
1072
1354
  * @param {string} entityType is the type of entity requested, such as "xtk:schema", "xtk:srcSchema", "xtk:navtree", "xtk:form", etc.
1073
1355
  * @param {string} fullName is the fully qualified name of the entity (i.e. <namespace>:<name>)
1074
1356
  * @param {string} representation the expected representation, or undefined to set the default
@@ -1091,7 +1373,7 @@ class Client {
1091
1373
 
1092
1374
  /**
1093
1375
  * Get a compiled schema (not a source schema) definition as a DOM or JSON object depending on hte current representation
1094
- *
1376
+ *
1095
1377
  * @param {string} schemaId the schema id, such as "xtk:session", or "nms:recipient"
1096
1378
  * @param {string} representation an optional representation of the schema: "BadgerFish", "SimpleJson" or "xml". If not set, we'll use the client default representation
1097
1379
  * @param {boolean} internal indicates an "internal" call, i.e. a call performed by the SDK itself rather than the user of the SDK. For instance, the SDK will dynamically load schemas to find method definitions
@@ -1101,18 +1383,19 @@ class Client {
1101
1383
  var that = this;
1102
1384
  var entity = that._entityCache.get("xtk:schema", schemaId);
1103
1385
  if (!entity) {
1104
- entity = await that.getEntityIfMoreRecent("xtk:schema", schemaId, "xml", internal);
1105
- }
1106
- if (entity)
1386
+ entity = await that.getEntityIfMoreRecent("xtk:schema", schemaId, "xml", internal);
1387
+ if (entity) {
1107
1388
  that._entityCache.put("xtk:schema", schemaId, entity);
1108
-
1389
+ that._methodCache.put(entity);
1390
+ }
1391
+ }
1109
1392
  entity = that._toRepresentation(entity, representation);
1110
1393
  return entity;
1111
1394
  }
1112
1395
 
1113
1396
  /**
1114
1397
  * Get the definition of a system enumeration (SysEnum). Will be returned as JSON or XML depending on the client 'representation' attribute
1115
- *
1398
+ *
1116
1399
  * @param {string} enumName
1117
1400
  * @param {string} optionalStartSchemaOrSchemaName
1118
1401
  * @returns {XML.XtkObject} the enumeration definition in the current representation
@@ -1138,13 +1421,13 @@ class Client {
1138
1421
  if (index == -1)
1139
1422
  throw CampaignException.BAD_PARAMETER("optionalStartSchemaOrSchemaName", optionalStartSchemaOrSchemaName, `getEnum expects a valid schema name. '${optionalStartSchemaOrSchemaName}' is not a valid name.`);
1140
1423
  optionalStartSchemaOrSchemaName = await this.getSchema(optionalStartSchemaOrSchemaName, undefined, true);
1141
- if (!optionalStartSchemaOrSchemaName)
1424
+ if (!optionalStartSchemaOrSchemaName)
1142
1425
  throw CampaignException.BAD_PARAMETER("optionalStartSchemaOrSchemaName", optionalStartSchemaOrSchemaName, `Schema '${optionalStartSchemaOrSchemaName}' not found.`);
1143
1426
  }
1144
- else
1427
+ else
1145
1428
  throw CampaignException.BAD_PARAMETER("optionalStartSchemaOrSchemaName", optionalStartSchemaOrSchemaName, `getEnum expects a valid schema name wich is a string. Given ${typeof optionalStartSchemaOrSchemaName} instead`);
1146
1429
 
1147
- const schema = optionalStartSchemaOrSchemaName;
1430
+ const schema = optionalStartSchemaOrSchemaName;
1148
1431
  for (const e of EntityAccessor.getChildElements(schema, "enumeration")) {
1149
1432
  const n = EntityAccessor.getAttributeAsString(e, "name");
1150
1433
  if (n == enumName)
@@ -1154,7 +1437,7 @@ class Client {
1154
1437
 
1155
1438
  /**
1156
1439
  * Call Campaign SOAP method
1157
- *
1440
+ *
1158
1441
  * @private
1159
1442
  * @param {string} methodName is the method to call. In order to be more JavaScript friendly, the first char can be lower-cased
1160
1443
  * @param {*} callContext the call context)
@@ -1165,7 +1448,7 @@ class Client {
1165
1448
  const that = this;
1166
1449
  const result = [];
1167
1450
  const schemaId = callContext.schemaId;
1168
-
1451
+
1169
1452
  var schema = await that.getSchema(schemaId, "xml", true);
1170
1453
  if (!schema)
1171
1454
  throw CampaignException.SOAP_UNKNOWN_METHOD(schemaId, methodName, `Schema '${schemaId}' not found`);
@@ -1260,7 +1543,7 @@ class Client {
1260
1543
  param = DomUtil.getNextSiblingElement(param, "param");
1261
1544
  }
1262
1545
  }
1263
-
1546
+
1264
1547
  return that._makeSoapCall(soapCall).then(function() {
1265
1548
  if (!isStatic) {
1266
1549
  // Non static methods, such as xtk:query#SelectAll return a element named "entity" which is the object itself on which
@@ -1359,30 +1642,40 @@ class Client {
1359
1642
  request.headers = request.headers || [];
1360
1643
  if (!request.headers['User-Agent'])
1361
1644
  request.headers['User-Agent'] = this._getUserAgentString();
1645
+ let event;
1362
1646
  try {
1363
1647
  const safeCallData = Util.trim(request.data);
1364
1648
  if (this._traceAPICalls)
1365
1649
  console.log(`HTTP//request ${request.method} ${request.url}${safeCallData ? " " + safeCallData : ""}`);
1366
1650
  this._notifyObservers((observer) => observer.onHTTPCall && observer.onHTTPCall(request, safeCallData) );
1651
+ event = this._trackEvent('HTTP//request', undefined, {
1652
+ url: request.url,
1653
+ method: request.method,
1654
+ headers: request.request,
1655
+ safeCallData: safeCallData,
1656
+ });
1367
1657
  const body = await this._transport(request);
1368
1658
 
1369
1659
  const safeCallResponse = Util.trim(body);
1370
1660
  if (this._traceAPICalls)
1371
1661
  console.log(`HTTP//response${safeCallResponse ? " " + safeCallResponse : ""}`);
1372
1662
  this._notifyObservers((observer) => observer.onHTTPCallSuccess && observer.onHTTPCallSuccess(request, safeCallResponse) );
1663
+ this._trackEvent('HTTP//response', event, { safeCallResponse: safeCallResponse });
1373
1664
  return body;
1374
1665
  } catch(err) {
1375
1666
  if (this._traceAPICalls)
1376
1667
  console.log("HTTP//failure", err);
1377
1668
  this._notifyObservers((observer) => observer.onHTTPCallFailure && observer.onHTTPCallFailure(request, err) );
1378
- throw makeCampaignException({ request:request, reqponse:err.response }, err);
1669
+ const ex = makeCampaignException({ request:request, reqponse:err.response }, err);
1670
+ this._trackEvent('HTTP//failure', event, { }, ex);
1671
+ throw ex;
1379
1672
  }
1380
1673
  }
1381
1674
 
1382
1675
  /**
1383
1676
  * Tests if the Campaign redirection server is up (/r/test).
1384
1677
  * Does not require a logged client
1385
- *
1678
+ *
1386
1679
  * @returns {Campaign.RedirStatus} an object describing the status of the redirection server
1387
1680
  */
1388
1681
  async test() {
@@ -1400,12 +1693,12 @@ class Client {
1400
1693
 
1401
1694
  /**
1402
1695
  * Ping the Campaign server (/nl/jsp/ping.jsp)
1403
- *
1696
+ *
1404
1697
  * @returns {Campaign.PingStatus} an object describing the server status
1405
1698
  */
1406
1699
  async ping() {
1407
1700
  const request = {
1408
- url: `${this._connectionParameters._endpoint}/nl/jsp/ping.jsp`,
1701
+ url: `${this._connectionParameters._endpoint}/nl/jsp/ping.jsp`,
1409
1702
  headers: {
1410
1703
  'X-Security-Token': this._securityToken,
1411
1704
  'Cookie': '__sessiontoken=' + this._sessionToken
@@ -1431,12 +1724,12 @@ class Client {
1431
1724
  /**
1432
1725
  * Ping a Message Center Campaign server (/nl/jsp/mcPing.jsp).
1433
1726
  * Assumes Message Center is installed
1434
- *
1727
+ *
1435
1728
  * @returns {Campaign.McPingStatus} an object describing Message Center server status
1436
1729
  */
1437
1730
  async mcPing() {
1438
1731
  const request = {
1439
- url: `${this._connectionParameters._endpoint}/nl/jsp/mcPing.jsp`,
1732
+ url: `${this._connectionParameters._endpoint}/nl/jsp/mcPing.jsp`,
1440
1733
  headers: {
1441
1734
  'X-Security-Token': this._securityToken,
1442
1735
  'Cookie': '__sessiontoken=' + this._sessionToken
@@ -1450,7 +1743,7 @@ class Client {
1450
1743
  const root = doc.documentElement;
1451
1744
  var status = lines[0].trim();
1452
1745
  if (status != "") root.setAttribute("status", status);
1453
-
1746
+
1454
1747
  var rtCount;
1455
1748
  var threshold;
1456
1749
  if (status == "Error") {
@@ -1493,4 +1786,4 @@ exports.Client = Client;
1493
1786
  exports.Credentials = Credentials;
1494
1787
  exports.ConnectionParameters = ConnectionParameters;
1495
1788
 
1496
- })();
1789
+ })();