posthog-node 3.2.1 → 3.4.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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 3.4.0 - 2024-01-09
2
+
3
+ 1. Numeric property handling for feature flags now does the expected: When passed in a number, we do a numeric comparison. When passed in a string, we do a string comparison. Previously, we always did a string comparison.
4
+ 2. Add support for relative date operators for local evaluation.
5
+
6
+ # 3.3.0 - 2024-01-02
7
+
8
+ 1. Adds PostHogSentryIntegration to allow automatic capturing of exceptions reported via the @sentry/node package
9
+
1
10
  # 3.2.1 - 2023-12-15
2
11
 
3
12
  1. Fixes issue where a background refresh of feature flags could throw an unhandled error. It now emits to be detected by `.on('error', ...)`
package/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './src/posthog-node'
2
+ export * from './src/extensions/sentry-integration'
package/lib/index.cjs.js CHANGED
@@ -158,7 +158,7 @@ function __spreadArray(to, from, pack) {
158
158
  return to.concat(ar || Array.prototype.slice.call(from));
159
159
  }
160
160
 
161
- var version = "3.2.1";
161
+ var version = "3.4.0";
162
162
 
163
163
  var PostHogPersistedProperty;
164
164
  (function (PostHogPersistedProperty) {
@@ -2348,12 +2348,36 @@ function matchProperty(property, propertyValues) {
2348
2348
 
2349
2349
  var overrideValue = propertyValues[key];
2350
2350
 
2351
+ function computeExactMatch(value, overrideValue) {
2352
+ if (Array.isArray(value)) {
2353
+ return value.map(function (val) {
2354
+ return String(val).toLowerCase();
2355
+ }).includes(String(overrideValue).toLowerCase());
2356
+ }
2357
+
2358
+ return String(value).toLowerCase() === String(overrideValue).toLowerCase();
2359
+ }
2360
+
2361
+ function compare(lhs, rhs, operator) {
2362
+ if (operator === 'gt') {
2363
+ return lhs > rhs;
2364
+ } else if (operator === 'gte') {
2365
+ return lhs >= rhs;
2366
+ } else if (operator === 'lt') {
2367
+ return lhs < rhs;
2368
+ } else if (operator === 'lte') {
2369
+ return lhs <= rhs;
2370
+ } else {
2371
+ throw new Error("Invalid operator: ".concat(operator));
2372
+ }
2373
+ }
2374
+
2351
2375
  switch (operator) {
2352
2376
  case 'exact':
2353
- return Array.isArray(value) ? value.indexOf(overrideValue) !== -1 : value === overrideValue;
2377
+ return computeExactMatch(value, overrideValue);
2354
2378
 
2355
2379
  case 'is_not':
2356
- return Array.isArray(value) ? value.indexOf(overrideValue) === -1 : value !== overrideValue;
2380
+ return !computeExactMatch(value, overrideValue);
2357
2381
 
2358
2382
  case 'is_set':
2359
2383
  return key in propertyValues;
@@ -2371,24 +2395,53 @@ function matchProperty(property, propertyValues) {
2371
2395
  return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null;
2372
2396
 
2373
2397
  case 'gt':
2374
- return typeof overrideValue == typeof value && overrideValue > value;
2375
-
2376
2398
  case 'gte':
2377
- return typeof overrideValue == typeof value && overrideValue >= value;
2378
-
2379
2399
  case 'lt':
2380
- return typeof overrideValue == typeof value && overrideValue < value;
2381
-
2382
2400
  case 'lte':
2383
- return typeof overrideValue == typeof value && overrideValue <= value;
2401
+ {
2402
+ // :TRICKY: We adjust comparison based on the override value passed in,
2403
+ // to make sure we handle both numeric and string comparisons appropriately.
2404
+ var parsedValue = typeof value === 'number' ? value : null;
2405
+
2406
+ if (typeof value === 'string') {
2407
+ try {
2408
+ parsedValue = parseFloat(value);
2409
+ } catch (err) {// pass
2410
+ }
2411
+ }
2412
+
2413
+ if (parsedValue != null && overrideValue != null) {
2414
+ // check both null and undefined
2415
+ if (typeof overrideValue === 'string') {
2416
+ return compare(overrideValue, String(value), operator);
2417
+ } else {
2418
+ return compare(overrideValue, parsedValue, operator);
2419
+ }
2420
+ } else {
2421
+ return compare(String(overrideValue), String(value), operator);
2422
+ }
2423
+ }
2384
2424
 
2385
2425
  case 'is_date_after':
2386
2426
  case 'is_date_before':
2427
+ case 'is_relative_date_before':
2428
+ case 'is_relative_date_after':
2387
2429
  {
2388
- var parsedDate = convertToDateTime(value);
2430
+ var parsedDate = null;
2431
+
2432
+ if (['is_relative_date_before', 'is_relative_date_after'].includes(operator)) {
2433
+ parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
2434
+ } else {
2435
+ parsedDate = convertToDateTime(value);
2436
+ }
2437
+
2438
+ if (parsedDate == null) {
2439
+ throw new InconclusiveMatchError("Invalid date: ".concat(value));
2440
+ }
2441
+
2389
2442
  var overrideDate = convertToDateTime(overrideValue);
2390
2443
 
2391
- if (operator === 'is_date_before') {
2444
+ if (['is_date_before', 'is_relative_date_before'].includes(operator)) {
2392
2445
  return overrideDate < parsedDate;
2393
2446
  }
2394
2447
 
@@ -2396,8 +2449,7 @@ function matchProperty(property, propertyValues) {
2396
2449
  }
2397
2450
 
2398
2451
  default:
2399
- console.error("Unknown operator: ".concat(operator));
2400
- return false;
2452
+ throw new InconclusiveMatchError("Unknown operator: ".concat(operator));
2401
2453
  }
2402
2454
  }
2403
2455
 
@@ -2539,6 +2591,45 @@ function convertToDateTime(value) {
2539
2591
  }
2540
2592
  }
2541
2593
 
2594
+ function relativeDateParseForFeatureFlagMatching(value) {
2595
+ var regex = /^(?<number>[0-9]+)(?<interval>[a-z])$/;
2596
+ var match = value.match(regex);
2597
+ var parsedDt = new Date(new Date().toISOString());
2598
+
2599
+ if (match) {
2600
+ if (!match.groups) {
2601
+ return null;
2602
+ }
2603
+
2604
+ var number = parseInt(match.groups['number']);
2605
+
2606
+ if (number >= 10000) {
2607
+ // Guard against overflow, disallow numbers greater than 10_000
2608
+ return null;
2609
+ }
2610
+
2611
+ var interval = match.groups['interval'];
2612
+
2613
+ if (interval == 'h') {
2614
+ parsedDt.setUTCHours(parsedDt.getUTCHours() - number);
2615
+ } else if (interval == 'd') {
2616
+ parsedDt.setUTCDate(parsedDt.getUTCDate() - number);
2617
+ } else if (interval == 'w') {
2618
+ parsedDt.setUTCDate(parsedDt.getUTCDate() - number * 7);
2619
+ } else if (interval == 'm') {
2620
+ parsedDt.setUTCMonth(parsedDt.getUTCMonth() - number);
2621
+ } else if (interval == 'y') {
2622
+ parsedDt.setUTCFullYear(parsedDt.getUTCFullYear() - number);
2623
+ } else {
2624
+ return null;
2625
+ }
2626
+
2627
+ return parsedDt;
2628
+ } else {
2629
+ return null;
2630
+ }
2631
+ }
2632
+
2542
2633
  var THIRTY_SECONDS = 30 * 1000;
2543
2634
  var MAX_CACHE_SIZE = 50 * 1000; // The actual exported Nodejs API.
2544
2635
 
@@ -3009,5 +3100,95 @@ function (_super) {
3009
3100
  return PostHog;
3010
3101
  }(PostHogCoreStateless);
3011
3102
 
3103
+ /**
3104
+ * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
3105
+ *
3106
+ * ### Usage
3107
+ *
3108
+ * Sentry.init({
3109
+ * dsn: 'https://example',
3110
+ * integrations: [
3111
+ * new PostHogSentryIntegration(posthog)
3112
+ * ]
3113
+ * })
3114
+ *
3115
+ * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
3116
+ *
3117
+ * @param {Object} [posthog] The posthog object
3118
+ * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
3119
+ * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
3120
+ * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
3121
+ */
3122
+ var PostHogSentryIntegration =
3123
+ /** @class */
3124
+ function () {
3125
+ function PostHogSentryIntegration(posthog, posthogHost, organization, prefix) {
3126
+ var _a;
3127
+
3128
+ this.posthog = posthog;
3129
+ this.posthogHost = posthogHost;
3130
+ this.organization = organization;
3131
+ this.prefix = prefix;
3132
+ this.name = 'posthog-node';
3133
+ this.posthogHost = (_a = posthog.options.host) !== null && _a !== void 0 ? _a : 'https://app.posthog.com';
3134
+ }
3135
+
3136
+ PostHogSentryIntegration.prototype.setupOnce = function (addGlobalEventProcessor, getCurrentHub) {
3137
+ var _this = this;
3138
+
3139
+ addGlobalEventProcessor(function (event) {
3140
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3141
+
3142
+ if (((_a = event.exception) === null || _a === void 0 ? void 0 : _a.values) === undefined || event.exception.values.length === 0) {
3143
+ return event;
3144
+ }
3145
+
3146
+ if (!event.tags) {
3147
+ event.tags = {};
3148
+ }
3149
+
3150
+ var sentry = getCurrentHub(); // Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
3151
+
3152
+ var userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG];
3153
+
3154
+ if (userId === undefined) {
3155
+ // If we can't find a user ID, don't bother linking the event. We won't be able to send anything meaningful to PostHog without it.
3156
+ return event;
3157
+ }
3158
+
3159
+ event.tags['PostHog Person URL'] = new URL("/person/".concat(userId), _this.posthogHost).toString();
3160
+ var properties = {
3161
+ // PostHog Exception Properties
3162
+ $exception_message: (_b = event.exception.values[0]) === null || _b === void 0 ? void 0 : _b.value,
3163
+ $exception_type: (_c = event.exception.values[0]) === null || _c === void 0 ? void 0 : _c.type,
3164
+ $exception_personURL: event.tags['PostHog Person URL'],
3165
+ // Sentry Exception Properties
3166
+ $sentry_event_id: event.event_id,
3167
+ $sentry_exception: event.exception,
3168
+ $sentry_exception_message: (_d = event.exception.values[0]) === null || _d === void 0 ? void 0 : _d.value,
3169
+ $sentry_exception_type: (_e = event.exception.values[0]) === null || _e === void 0 ? void 0 : _e.type,
3170
+ $sentry_tags: event.tags
3171
+ };
3172
+ var projectId = (_g = (_f = sentry.getClient()) === null || _f === void 0 ? void 0 : _f.getDsn()) === null || _g === void 0 ? void 0 : _g.projectId;
3173
+
3174
+ if (_this.organization !== undefined && projectId !== undefined && event.event_id !== undefined) {
3175
+ properties.$sentry_url = "".concat((_h = _this.prefix) !== null && _h !== void 0 ? _h : 'https://sentry.io/organizations', "/").concat(_this.organization, "/issues/?project=").concat(projectId, "&query=").concat(event.event_id);
3176
+ }
3177
+
3178
+ _this.posthog.capture({
3179
+ event: '$exception',
3180
+ distinctId: userId,
3181
+ properties: properties
3182
+ });
3183
+
3184
+ return event;
3185
+ });
3186
+ };
3187
+
3188
+ PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
3189
+ return PostHogSentryIntegration;
3190
+ }();
3191
+
3012
3192
  exports.PostHog = PostHog;
3193
+ exports.PostHogSentryIntegration = PostHogSentryIntegration;
3013
3194
  //# sourceMappingURL=index.cjs.js.map