@mparticle/web-sdk 2.28.2 → 2.29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "2.28.2",
3
+ "version": "2.29.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -0,0 +1,89 @@
1
+ import { IAliasRequest, IAliasCallback } from "./identity.interfaces";
2
+ import { MParticleWebSDK } from "./sdkRuntimeModels";
3
+ import Constants from './constants';
4
+ import { FetchUploader, XHRUploader } from './uploaders';
5
+ import { HTTP_ACCEPTED, HTTP_OK } from "./constants";
6
+
7
+
8
+ const { HTTPCodes, Messages } = Constants;
9
+
10
+ interface IAliasResponseBody {
11
+ message?: string
12
+ }
13
+
14
+ export async function sendAliasRequest (mpInstance: MParticleWebSDK, aliasRequest: IAliasRequest, aliasCallback: IAliasCallback) {
15
+ const { verbose, error } = mpInstance.Logger;
16
+ const { invokeAliasCallback } = mpInstance._Helpers;
17
+ const { aliasUrl } = mpInstance._Store.SDKConfig;
18
+ const { devToken: apiKey } = mpInstance._Store;
19
+
20
+ verbose(Messages.InformationMessages.SendAliasHttp);
21
+
22
+ // https://go.mparticle.com/work/SQDSDKS-6750
23
+ const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`;
24
+ const uploader = window.fetch
25
+ ? new FetchUploader(uploadUrl)
26
+ : new XHRUploader(uploadUrl);
27
+
28
+
29
+ // https://go.mparticle.com/work/SQDSDKS-6568
30
+ const uploadPayload = {
31
+ method: 'post',
32
+ headers: {
33
+ Accept: 'text/plain;charset=UTF-8',
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ body: JSON.stringify(aliasRequest),
37
+ };
38
+
39
+ try {
40
+ const response = await uploader.upload(uploadPayload);
41
+
42
+ let message: string;
43
+ let aliasResponseBody: IAliasResponseBody;
44
+
45
+ // FetchUploader returns the response as a JSON object that we have to await
46
+ if (response.json) {
47
+ // HTTP responses of 202, 200, and 403 do not have a response. response.json will always exist on a fetch, but can only be await-ed when the response is not empty, otherwise it will throw an error.
48
+ try {
49
+ aliasResponseBody = await response.json();
50
+ } catch (e) {
51
+ verbose('The request has no response body');
52
+ }
53
+ } else {
54
+ // https://go.mparticle.com/work/SQDSDKS-6568
55
+ // XHRUploader returns the response as a string that we need to parse
56
+ const xhrResponse = response as unknown as XMLHttpRequest;
57
+ // debugger;
58
+ aliasResponseBody = xhrResponse.responseText
59
+ ? JSON.parse(xhrResponse.responseText)
60
+ : '';
61
+ }
62
+
63
+ let errorMessage: string;
64
+
65
+ switch (response.status) {
66
+ case HTTP_OK:
67
+ case HTTP_ACCEPTED:
68
+ // https://go.mparticle.com/work/SQDSDKS-6670
69
+ message =
70
+ 'Successfully sent forwarding stats to mParticle Servers';
71
+ break;
72
+ default:
73
+ // 400 has an error message, but 403 doesn't
74
+ if (aliasResponseBody?.message) {
75
+ errorMessage = aliasResponseBody.message;
76
+ }
77
+ message =
78
+ 'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' +
79
+ response.status;
80
+ }
81
+
82
+ verbose(message);
83
+ invokeAliasCallback(aliasCallback, response.status, errorMessage);
84
+ } catch (e) {
85
+ const err = e as Error;
86
+ error('Error sending alias request to mParticle servers. ' + err);
87
+ invokeAliasCallback(aliasCallback, HTTPCodes.noHttpCoverage, (err.message));
88
+ }
89
+ };
package/src/constants.ts CHANGED
@@ -174,6 +174,7 @@ const Constants = {
174
174
  DirectUrlRouting: 'directURLRouting',
175
175
  CacheIdentity: 'cacheIdentity',
176
176
  AudienceAPI: 'audienceAPI',
177
+ CaptureIntegrationSpecificIds: 'captureIntegrationSpecificIds',
177
178
  },
178
179
  DefaultInstance: 'default_instance',
179
180
  CCPAPurpose: 'data_sale_opt_out',
@@ -189,4 +190,10 @@ export default Constants;
189
190
 
190
191
  // https://go.mparticle.com/work/SQDSDKS-6080
191
192
  export const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
192
- export const MILLIS_IN_ONE_SEC = 1000;
193
+ export const MILLIS_IN_ONE_SEC = 1000;
194
+
195
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
196
+ export const HTTP_OK = 200 as const;
197
+ export const HTTP_ACCEPTED = 202 as const;
198
+ export const HTTP_BAD_REQUEST = 400 as const;
199
+ export const HTTP_FORBIDDEN = 403 as const;
@@ -55,7 +55,7 @@ export interface IIdentityAPIRequestData {
55
55
  context: string | null;
56
56
  environment: Environment;
57
57
  request_id: string;
58
- reqest_timestamp_unixtime_ms: number;
58
+ request_timestamp_unixtime_ms: number;
59
59
  previous_mpid: MPID | null;
60
60
  known_identities: IKnownIdentities;
61
61
  }
@@ -115,6 +115,15 @@ export interface IAliasRequest {
115
115
  scope?: string;
116
116
  }
117
117
 
118
+ export interface IAliasCallback {
119
+ (result: IAliasResult): void;
120
+ }
121
+
122
+ export interface IAliasResult {
123
+ httpCode: number;
124
+ message: string;
125
+ }
126
+
118
127
  export interface SDKIdentityApi {
119
128
  HTTPCodes: typeof HTTPCodes;
120
129
  identify?(
package/src/identity.js CHANGED
@@ -1,4 +1,4 @@
1
- import Constants from './constants';
1
+ import Constants, { HTTP_OK } from './constants';
2
2
  import Types from './types';
3
3
  import {
4
4
  cacheOrClearIdCache,
@@ -13,7 +13,6 @@ const { CacheIdentity } = FeatureFlags;
13
13
  const { Identify, Modify, Login, Logout } = IdentityMethods;
14
14
  import {
15
15
  generateDeprecationMessage,
16
- HTTP_SUCCESS,
17
16
  isEmpty,
18
17
  isFunction,
19
18
  isObject,
@@ -1513,7 +1512,7 @@ export default function Identity(mpInstance) {
1513
1512
  );
1514
1513
  }
1515
1514
 
1516
- if (identityResponse.status === HTTP_SUCCESS) {
1515
+ if (identityResponse.status === HTTP_OK) {
1517
1516
  if (getFeatureFlag(CacheIdentity)) {
1518
1517
  const identityResponseForCache = xhrIdentityResponseAdapter(
1519
1518
  identityResponse
@@ -1,5 +1,6 @@
1
1
  import Constants from './constants';
2
2
  import { xhrIdentityResponseAdapter } from './identity-utils';
3
+ import { sendAliasRequest } from './aliasRequestApiClient';
3
4
 
4
5
  var HTTPCodes = Constants.HTTPCodes,
5
6
  Messages = Constants.Messages;
@@ -7,59 +8,8 @@ var HTTPCodes = Constants.HTTPCodes,
7
8
  const { Modify } = Constants.IdentityMethods;
8
9
 
9
10
  export default function IdentityAPIClient(mpInstance) {
10
- this.sendAliasRequest = function(aliasRequest, callback) {
11
- var xhr,
12
- xhrCallback = function() {
13
- if (xhr.readyState === 4) {
14
- // https://go.mparticle.com/work/SQDSDKS-6368
15
- mpInstance.Logger.verbose(
16
- 'Received ' + xhr.statusText + ' from server'
17
- );
18
- //only parse error messages from failing requests
19
- if (xhr.status !== 200 && xhr.status !== 202) {
20
- if (xhr.responseText) {
21
- var response = JSON.parse(xhr.responseText);
22
- if (response.hasOwnProperty('message')) {
23
- var errorMessage = response.message;
24
- mpInstance._Helpers.invokeAliasCallback(
25
- callback,
26
- xhr.status,
27
- errorMessage
28
- );
29
- return;
30
- }
31
- }
32
- }
33
- mpInstance._Helpers.invokeAliasCallback(
34
- callback,
35
- xhr.status
36
- );
37
- }
38
- };
39
- mpInstance.Logger.verbose(Messages.InformationMessages.SendAliasHttp);
40
-
41
- xhr = mpInstance._Helpers.createXHR(xhrCallback);
42
- if (xhr) {
43
- try {
44
- xhr.open(
45
- 'post',
46
- mpInstance._Helpers.createServiceUrl(
47
- mpInstance._Store.SDKConfig.aliasUrl,
48
- mpInstance._Store.devToken
49
- ) + '/Alias'
50
- );
51
- xhr.send(JSON.stringify(aliasRequest));
52
- } catch (e) {
53
- mpInstance._Helpers.invokeAliasCallback(
54
- callback,
55
- HTTPCodes.noHttpCoverage,
56
- e
57
- );
58
- mpInstance.Logger.error(
59
- 'Error sending alias request to mParticle servers. ' + e
60
- );
61
- }
62
- }
11
+ this.sendAliasRequest = async function(aliasRequest, callback) {
12
+ await sendAliasRequest(mpInstance, aliasRequest, callback);
63
13
  };
64
14
 
65
15
  this.sendIdentityRequest = function(
@@ -0,0 +1,153 @@
1
+ import { SDKEventCustomFlags } from './sdkRuntimeModels';
2
+ import {
3
+ Dictionary,
4
+ queryStringParser,
5
+ getCookies,
6
+ getHref,
7
+ isEmpty,
8
+ } from './utils';
9
+
10
+ export interface IntegrationCaptureProcessorFunction {
11
+ (clickId: string, url: string, timestamp?: number): string;
12
+ }
13
+
14
+ // Facebook Click ID has specific formatting rules
15
+ // The formatted ClickID value must be of the form version.subdomainIndex.creationTime.<fbclid>, where:
16
+ // - version is always this prefix: fb
17
+ // - subdomainIndex is which domain the cookie is defined on ('com' = 0, 'example.com' = 1, 'www.example.com' = 2)
18
+ // - creationTime is the UNIX time since epoch in milliseconds when the _fbc was stored. If you don't save the _fbc cookie, use the timestamp when you first observed or received this fbclid value
19
+ // - <fbclid> is the value for the fbclid query parameter in the page URL.
20
+ // https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
21
+ export const facebookClickIdProcessor: IntegrationCaptureProcessorFunction = (
22
+ clickId: string,
23
+ url: string,
24
+ timestamp?: number,
25
+ ): string => {
26
+ if (!clickId || !url) {
27
+ return '';
28
+ }
29
+
30
+ const urlSegments = url?.split('//')
31
+ if (!urlSegments) {
32
+ return '';
33
+ }
34
+
35
+ const urlParts = urlSegments[1].split('/');
36
+ const domainParts = urlParts[0].split('.');
37
+ let subdomainIndex: number = 1;
38
+
39
+ // The rules for subdomainIndex are for parsing the domain portion
40
+ // of the URL for cookies, but in this case we are parsing the URL
41
+ // itself, so we can ignore the use of 0 for 'com'
42
+ if (domainParts.length >= 3) {
43
+ subdomainIndex = 2;
44
+ }
45
+
46
+ // If timestamp is not provided, use the current time
47
+ const _timestamp = timestamp || Date.now();
48
+
49
+ return `fb.${subdomainIndex}.${_timestamp}.${clickId}`;
50
+ };
51
+ interface IntegrationIdMapping {
52
+ [key: string]: {
53
+ mappedKey: string;
54
+ processor?: IntegrationCaptureProcessorFunction;
55
+ };
56
+ }
57
+
58
+ const integrationMapping: IntegrationIdMapping = {
59
+ fbclid: {
60
+ mappedKey: 'Facebook.ClickId',
61
+ processor: facebookClickIdProcessor,
62
+ },
63
+ _fbp: {
64
+ mappedKey: 'Facebook.BrowserId',
65
+ },
66
+ _fbc: {
67
+ mappedKey: 'Facebook.ClickId',
68
+ },
69
+ };
70
+
71
+ export default class IntegrationCapture {
72
+ public clickIds: Dictionary<string>;
73
+ public readonly initialTimestamp: number;
74
+
75
+ constructor() {
76
+ this.initialTimestamp = Date.now();
77
+ }
78
+
79
+ /**
80
+ * Captures Integration Ids from cookies and query params and stores them in clickIds object
81
+ */
82
+ public capture(): void {
83
+ const queryParams = this.captureQueryParams() || {};
84
+ const cookies = this.captureCookies() || {};
85
+
86
+ // Exclude _fbc if fbclid is present
87
+ // https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc#retrieve-from-fbclid-url-query-parameter
88
+ if (queryParams['fbclid'] && cookies['_fbc']) {
89
+ delete cookies['_fbc'];
90
+ }
91
+
92
+ this.clickIds = { ...this.clickIds, ...queryParams, ...cookies };
93
+ }
94
+
95
+ /**
96
+ * Captures cookies based on the integration ID mapping.
97
+ */
98
+ public captureCookies(): Dictionary<string> {
99
+ const cookies = getCookies(Object.keys(integrationMapping));
100
+ return this.applyProcessors(cookies);
101
+ }
102
+
103
+ /**
104
+ * Captures query parameters based on the integration ID mapping.
105
+ */
106
+ public captureQueryParams(): Dictionary<string> {
107
+ const queryParams = this.getQueryParams();
108
+ return this.applyProcessors(queryParams, getHref(), this.initialTimestamp);
109
+ }
110
+
111
+ /**
112
+ * Gets the query parameters based on the integration ID mapping.
113
+ * @returns {Dictionary<string>} The query parameters.
114
+ */
115
+ public getQueryParams(): Dictionary<string> {
116
+ return queryStringParser(getHref(), Object.keys(integrationMapping));
117
+ }
118
+
119
+ /**
120
+ * Converts captured click IDs to custom flags for SDK events.
121
+ * @returns {SDKEventCustomFlags} The custom flags.
122
+ */
123
+ public getClickIdsAsCustomFlags(): SDKEventCustomFlags {
124
+ const customFlags: SDKEventCustomFlags = {};
125
+
126
+ if (!this.clickIds) {
127
+ return customFlags;
128
+ }
129
+
130
+ for (const [key, value] of Object.entries(this.clickIds)) {
131
+ const mappedKey = integrationMapping[key]?.mappedKey;
132
+ if (!isEmpty(mappedKey)) {
133
+ customFlags[mappedKey] = value;
134
+ }
135
+ }
136
+ return customFlags;
137
+ }
138
+
139
+ private applyProcessors(clickIds: Dictionary<string>, url?: string, timestamp?: number): Dictionary<string> {
140
+ const processedClickIds: Dictionary<string> = {};
141
+
142
+ for (const [key, value] of Object.entries(clickIds)) {
143
+ const processor = integrationMapping[key]?.processor;
144
+ if (processor) {
145
+ processedClickIds[key] = processor(value, url, timestamp);
146
+ } else {
147
+ processedClickIds[key] = value;
148
+ }
149
+ }
150
+
151
+ return processedClickIds;
152
+ }
153
+ }
@@ -39,9 +39,10 @@ import IdentityAPIClient from './identityApiClient';
39
39
  import { isEmpty, isFunction } from './utils';
40
40
  import { LocalStorageVault } from './vault';
41
41
  import { removeExpiredIdentityCacheDates } from './identity-utils';
42
+ import IntegrationCapture from './integrationCapture';
42
43
 
43
44
  const { Messages, HTTPCodes, FeatureFlags } = Constants;
44
- const { ReportBatching } = FeatureFlags;
45
+ const { ReportBatching, CaptureIntegrationSpecificIds } = FeatureFlags;
45
46
  const { StartingInitialization } = Messages.InformationMessages;
46
47
 
47
48
  /**
@@ -77,6 +78,7 @@ export default function mParticleInstance(instanceName) {
77
78
  integrationDelays: {},
78
79
  forwarderConstructors: [],
79
80
  };
81
+ this._IntegrationCapture = new IntegrationCapture();
80
82
 
81
83
  // required for forwarders once they reference the mparticle instance
82
84
  this.IdentityType = Types.IdentityType;
@@ -1336,6 +1338,10 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1336
1338
  mpInstance._ForwardingStatsUploader.startForwardingStatsTimer();
1337
1339
  }
1338
1340
 
1341
+ if (mpInstance._Helpers.getFeatureFlag(CaptureIntegrationSpecificIds)) {
1342
+ mpInstance._IntegrationCapture.capture();
1343
+ }
1344
+
1339
1345
  mpInstance._Forwarders.processForwarders(
1340
1346
  config,
1341
1347
  mpInstance._APIClient.prepareForwardingStats
@@ -16,7 +16,11 @@ import { IPersistence } from './persistence.interfaces';
16
16
  import { IMPSideloadedKit } from './sideloadedKit';
17
17
  import { ISessionManager } from './sessionManager';
18
18
  import { Kit, MPForwarder } from './forwarders.interfaces';
19
- import { IIdentity, SDKIdentityApi } from './identity.interfaces';
19
+ import {
20
+ IIdentity,
21
+ SDKIdentityApi,
22
+ IAliasCallback,
23
+ } from './identity.interfaces';
20
24
  import {
21
25
  ISDKUserAttributeChangeData,
22
26
  ISDKUserIdentityChanges,
@@ -26,6 +30,7 @@ import {
26
30
  ISDKUserAttributes,
27
31
  } from './identity-user-interfaces';
28
32
  import { IIdentityType } from './types.interfaces';
33
+ import IntegrationCapture from './integrationCapture';
29
34
 
30
35
  // TODO: Resolve this with version in @mparticle/web-sdk
31
36
  export type SDKEventCustomFlags = Dictionary<any>;
@@ -146,6 +151,7 @@ interface IEvents {
146
151
 
147
152
  export interface MParticleWebSDK {
148
153
  addForwarder(mockForwarder: MPForwarder): void;
154
+ _IntegrationCapture: IntegrationCapture;
149
155
  IdentityType: IIdentityType;
150
156
  _Identity: IIdentity;
151
157
  Identity: SDKIdentityApi;
@@ -265,6 +271,11 @@ export interface SDKHelpersApi {
265
271
  generateHash?(value: string): string;
266
272
  // https://go.mparticle.com/work/SQDSDKS-6317
267
273
  getFeatureFlag?(feature: string): boolean | string; // TODO: Feature Constants should be converted to enum
274
+ invokeAliasCallback(
275
+ aliasCallback: IAliasCallback,
276
+ number: number,
277
+ errorMessage: string
278
+ ): void;
268
279
  isDelayedByIntegration?(
269
280
  delayedIntegrations: Dictionary<boolean>,
270
281
  timeoutStart: number,
@@ -8,6 +8,7 @@ import {
8
8
  BaseEvent,
9
9
  MParticleWebSDK,
10
10
  SDKEvent,
11
+ SDKEventCustomFlags,
11
12
  SDKGeoLocation,
12
13
  SDKProduct,
13
14
  } from './sdkRuntimeModels';
@@ -317,6 +318,18 @@ export default function ServerModel(
317
318
  event.messageType === Types.MessageType.OptOut ||
318
319
  mpInstance._Store.webviewBridgeEnabled
319
320
  ) {
321
+ let customFlags: SDKEventCustomFlags = {...event.customFlags};
322
+
323
+ // https://go.mparticle.com/work/SQDSDKS-5053
324
+ if (mpInstance._Helpers.getFeatureFlag && mpInstance._Helpers.getFeatureFlag(Constants.FeatureFlags.CaptureIntegrationSpecificIds)) {
325
+
326
+ // Attempt to recapture click IDs in case a third party integration
327
+ // has added or updated new click IDs since the last event was sent.
328
+ mpInstance._IntegrationCapture.capture();
329
+ const transformedClickIDs = mpInstance._IntegrationCapture.getClickIdsAsCustomFlags();
330
+ customFlags = {...transformedClickIDs, ...customFlags};
331
+ }
332
+
320
333
  if (event.hasOwnProperty('toEventAPIObject')) {
321
334
  eventObject = event.toEventAPIObject();
322
335
  } else {
@@ -336,7 +349,7 @@ export default function ServerModel(
336
349
  event.sourceMessageId ||
337
350
  mpInstance._Helpers.generateUniqueId(),
338
351
  EventDataType: event.messageType,
339
- CustomFlags: event.customFlags || {},
352
+ CustomFlags: customFlags,
340
353
  UserAttributeChanges: event.userAttributeChanges,
341
354
  UserIdentityChanges: event.userIdentityChanges,
342
355
  };
package/src/store.ts CHANGED
@@ -710,6 +710,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
710
710
  DirectUrlRouting,
711
711
  CacheIdentity,
712
712
  AudienceAPI,
713
+ CaptureIntegrationSpecificIds,
713
714
  } = Constants.FeatureFlags;
714
715
 
715
716
  if (!config.flags) {
@@ -727,6 +728,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
727
728
  flags[DirectUrlRouting] = config.flags[DirectUrlRouting] === 'True';
728
729
  flags[CacheIdentity] = config.flags[CacheIdentity] === 'True';
729
730
  flags[AudienceAPI] = config.flags[AudienceAPI] === 'True';
731
+ flags[CaptureIntegrationSpecificIds] = config.flags[CaptureIntegrationSpecificIds] === 'True';
730
732
 
731
733
  return flags;
732
734
  }
package/src/uploaders.ts CHANGED
@@ -47,6 +47,7 @@ export class XHRUploader extends AsyncUploader {
47
47
  // 3 LOADING Downloading; responseText holds partial data.
48
48
  // 4 DONE The operation is complete.
49
49
 
50
+ // https://go.mparticle.com/work/SQDSDKS-6736
50
51
  private async makeRequest(
51
52
  url: string,
52
53
  data: string,
@@ -58,13 +59,17 @@ export class XHRUploader extends AsyncUploader {
58
59
  if (xhr.readyState !== 4) return;
59
60
 
60
61
  // Process the response
61
- if (xhr.status >= 200 && xhr.status < 300) {
62
- resolve(xhr as unknown as Response);
63
- } else {
64
- reject(xhr);
65
- }
62
+ // We resolve all xhr responses whose ready state is 4 regardless of HTTP codes that may be errors (400+)
63
+ // because these are valid HTTP responses.
64
+ resolve((xhr as unknown) as Response);
66
65
  };
67
66
 
67
+ // Reject a promise only when there is an xhr error
68
+ xhr.onerror = () => {
69
+ reject((xhr as unknown) as Response);
70
+ };
71
+
72
+
68
73
  xhr.open(method, url);
69
74
  xhr.send(data);
70
75
  });
package/src/utils.ts CHANGED
@@ -9,8 +9,6 @@ export type Dictionary<V = any> = Record<string, V>;
9
9
 
10
10
  export type Environment = 'development' | 'production';
11
11
 
12
- export const HTTP_SUCCESS = 200 as const;
13
-
14
12
  const createCookieString = (value: string): string =>
15
13
  replaceCommasWithPipes(replaceQuotesWithApostrophes(value));
16
14
 
@@ -224,7 +222,7 @@ const isDataPlanSlug = (str: string): boolean => str === toDataPlanSlug(str);
224
222
  const isStringOrNumber = (value: any): boolean =>
225
223
  isString(value) || isNumber(value);
226
224
 
227
- const isEmpty = (value: Dictionary<any> | null | undefined): boolean =>
225
+ const isEmpty = (value: Dictionary<any> | string | null | undefined): boolean =>
228
226
  value == null || !(Object.keys(value) || value).length;
229
227
 
230
228
  const mergeObjects = <T extends object>(...objects: T[]): T => {
@@ -234,6 +232,107 @@ const mergeObjects = <T extends object>(...objects: T[]): T => {
234
232
  const moveElementToEnd = <T>(array: T[], index: number): T[] =>
235
233
  array.slice(0, index).concat(array.slice(index + 1), array[index]);
236
234
 
235
+ const queryStringParser = (
236
+ url: string,
237
+ keys: string[] = []
238
+ ): Dictionary<string> => {
239
+ let urlParams: URLSearchParams | URLSearchParamsFallback;
240
+ let results: Dictionary<string> = {};
241
+
242
+ if (!url) return results;
243
+
244
+ if (typeof URL !== 'undefined' && typeof URLSearchParams !== 'undefined') {
245
+ const urlObject = new URL(url);
246
+ urlParams = new URLSearchParams(urlObject.search);
247
+ } else {
248
+ urlParams = queryStringParserFallback(url);
249
+ }
250
+
251
+ if (isEmpty(keys)) {
252
+ urlParams.forEach((value, key) => {
253
+ results[key] = value;
254
+ });
255
+ } else {
256
+ keys.forEach(key => {
257
+ const value = urlParams.get(key);
258
+ if (value) {
259
+ results[key] = value;
260
+ }
261
+ });
262
+ }
263
+
264
+ return results;
265
+ };
266
+
267
+ interface URLSearchParamsFallback {
268
+ get: (key: string) => string | null;
269
+ forEach: (callback: (value: string, key: string) => void) => void;
270
+ }
271
+
272
+ const queryStringParserFallback = (url: string): URLSearchParamsFallback => {
273
+ let params: Dictionary<string> = {};
274
+ const queryString = url.split('?')[1] || '';
275
+ const pairs = queryString.split('&');
276
+
277
+ pairs.forEach(pair => {
278
+ var [key, value] = pair.split('=');
279
+ if (key && value) {
280
+ params[key] = decodeURIComponent(value || '');
281
+ }
282
+ });
283
+
284
+ return {
285
+ get: function(key: string) {
286
+ return params[key];
287
+ },
288
+ forEach: function(callback: (value: string, key: string) => void) {
289
+ for (var key in params) {
290
+ if (params.hasOwnProperty(key)) {
291
+ callback(params[key], key);
292
+ }
293
+ }
294
+ },
295
+ };
296
+ };
297
+
298
+ // Get cookies as a dictionary
299
+ const getCookies = (keys?: string[]): Dictionary<string> => {
300
+ // Helper function to parse cookies from document.cookie
301
+ const parseCookies = (): string[] => {
302
+ if (typeof window === 'undefined') {
303
+ return [];
304
+ }
305
+ return window.document.cookie.split(';').map(cookie => cookie.trim());
306
+ };
307
+
308
+ // Helper function to filter cookies by keys
309
+ const filterCookies = (
310
+ cookies: string[],
311
+ keys?: string[]
312
+ ): Dictionary<string> => {
313
+ const results: Dictionary<string> = {};
314
+ for (const cookie of cookies) {
315
+ const [key, value] = cookie.split('=');
316
+ if (!keys || keys.includes(key)) {
317
+ results[key] = value;
318
+ }
319
+ }
320
+ return results;
321
+ };
322
+
323
+ // Parse cookies from document.cookie
324
+ const parsedCookies: string[] = parseCookies();
325
+
326
+ // Filter cookies by keys if provided
327
+ return filterCookies(parsedCookies, keys);
328
+ };
329
+
330
+ const getHref = (): string => {
331
+ return typeof window !== 'undefined' && window.location
332
+ ? window.location.href
333
+ : '';
334
+ };
335
+
237
336
  export {
238
337
  createCookieString,
239
338
  revertCookieString,
@@ -264,4 +363,7 @@ export {
264
363
  isValidCustomFlagProperty,
265
364
  mergeObjects,
266
365
  moveElementToEnd,
366
+ queryStringParser,
367
+ getCookies,
368
+ getHref,
267
369
  };