All files / src/core/bindings AnalyticsBinding.ts

100% Statements 18/18
100% Branches 2/2
100% Functions 4/4
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 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 89 90 91 92 93 94 95 96 97 98 99 100 10128x 28x 28x 28x 28x                                                           28x               34x   34x                     19x                                                       19x 19x   14x   5x   5x 4x   1x     5x        
import { Logger } from '@bayudwiyansatria/core'
import { Binding } from '@/core/bindings/Binding'
import { events } from '@/constants/Events'
import { MissingBindingError } from '@/exceptions/MissingBindingError'
import { lazySettings } from '@/utils/lazySettings'
import { AnalyticsSettings } from '@/types/AnalyticsSettings'
import type { CloudflareEnv } from '@/types/CloudflareEnv'
 
/**
 * Accessor for the Analytics Engine dataset binding
 * (`analytics_engine_datasets` in `wrangler.json`).
 *
 * The dataset carries telemetry — measurements to aggregate — and never log
 * lines: Analytics Engine samples at volume, so an individual line written here
 * may simply not be there when it is needed. Logging belongs to `Logger` in
 * `@bayudwiyansatria/core`, which writes to the runtime's log stream instead.
 *
 * @example
 * ```ts
 * const dataset = new AnalyticsBinding() // binding name from the configuration layer
 *
 * dataset.writeSafe(ctx.env, {
 *   indexes: ['/api/v1/articles'], // sampling key
 *   blobs: ['POST', '201'], // dimensions to filter by
 *   doubles: [42, 1] // duration and a unit count
 * })
 * ```
 *
 * @class
 *
 * @author Bayu Dwiyan Satria
 * @version 1.0.0
 * @since 1.0.0
 */
export class AnalyticsBinding<E extends CloudflareEnv = CloudflareEnv> extends Binding<AnalyticsEngineDataset, E> {
  /**
   * Constructs an AnalyticsBinding instance.
   *
   * @param settings Resolved settings. Defaults to `resolve('analytics')`, so callers
   *   normally construct this with no arguments at all.
   */
  constructor(settings?: AnalyticsSettings) {
    const settle = lazySettings<AnalyticsSettings>('analytics', settings)
 
    super(() => settle().binding)
  }
 
  /**
   * Writes a data point to the dataset.
   *
   * @param env The Worker environment.
   * @param event The data point — blobs, doubles, and indexes.
   * @throws {@link MissingBindingError} When the Analytics Engine binding is absent.
   */
  public write(env: E, event: AnalyticsEngineDataPoint): void {
    this.resolve(env).writeDataPoint(event)
  }
 
  /**
   * Writes a data point, swallowing any failure.
   *
   * Telemetry is never worth failing a request over, so a missing binding or a
   * rejected write is logged and reported through the return value instead of
   * propagating.
   *
   * @remarks
   * The two failures are not the same event, and conflating them is what made
   * the earlier version of this method unreadable in production. A Worker with
   * no `TELEMETRY` dataset bound is *configured that way* — it is the degraded
   * path this accessor documents — so an error line per request would be a
   * permanent, sampled, billed restatement of a deployment decision. A write
   * that was accepted and then rejected is a real fault, and rare enough to be
   * worth a line.
   *
   * Both go through `Logger` rather than `console.error` directly, so the line
   * is one indexed JSON object like every other and `analytics.error` is a
   * filter rather than a substring search.
   *
   * @param env The Worker environment.
   * @param event The data point — blobs, doubles, and indexes.
   * @returns `true` when the write was accepted, `false` when it failed.
   */
  public writeSafe(env: E, event: AnalyticsEngineDataPoint): boolean {
    try {
      this.write(env, event)
 
      return true
    } catch (e) {
      const log = Logger.fromEnv(env, { component: 'AnalyticsBinding' })
 
      if (e instanceof MissingBindingError) {
        log.debug(events.ANALYTICS_ERROR, { reason: 'binding_absent', binding: this.name })
      } else {
        log.warn(events.ANALYTICS_ERROR, { reason: 'write_rejected', error: e })
      }
 
      return false
    }
  }
}