PageSourceSearch

https://chairkick.com/assets/controllers/first_recording_conversion_controller-de6c767b.js

js chairkick.com collected 2026-10-02 00:48:29 UTC 4,631 bytes, 125 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Budgets survive Turbo visits/remounts within this document. A full page load
4// can retry pending delivery with the same durable per-user transaction ID.
5const dispatches = new Map()
6const POLL_INTERVAL = 15000
7const CALLBACK_RETRY_DELAY = 30000
8const MAX_TRACKING_TIME = 5 * 60 * 1000
9const MAX_DISPATCH_ATTEMPTS = 3
10const MAX_ACKNOWLEDGMENT_ATTEMPTS = 3
11
12export default class extends Controller {
13  static values = { url: String, transactionId: String }
14
15  connect() {
16    if (document.documentElement.hasAttribute("data-turbo-preview")) return
17    this.checking = false
18    this.finished = false
19    if (!dispatches.has(this.transactionIdValue)) {
20      dispatches.set(this.transactionIdValue, {
21        expiresAt: Date.now() + MAX_TRACKING_TIME,
22        dispatchAttempts: 0,
23        acknowledgmentAttempts: 0,
24        callbackReceived: false
25      })
26    }
27    this.delivery = dispatches.get(this.transactionIdValue)
28    this.deadline = setTimeout(() => this.finish(), Math.max(0, this.delivery.expiresAt - Date.now()))
29    this.onVisible = () => this.check()
30    document.addEventListener("visibilitychange", this.onVisible)
31    this.timer = setInterval(() => this.check(), POLL_INTERVAL)
32    this.check()
33  }
34
35  disconnect() {
36    clearInterval(this.timer)
37    clearTimeout(this.deadline)
38    document.removeEventListener("visibilitychange", this.onVisible)
39  }
40
41  async check() {
42    if (this.finished || this.checking || document.visibilityState !== "visible") return
43    if (Date.now() >= this.delivery.expiresAt ||
44        this.delivery.acknowledgmentAttempts >= MAX_ACKNOWLEDGMENT_ATTEMPTS ||
45        (!this.delivery.callbackReceived && this.delivery.dispatchAttempts >= MAX_DISPATCH_ATTEMPTS)) return this.finish()
46    this.checking = true
47
48    try {
49      const response = await this.request()
50      if (response.status === 401) return this.finish()
51      if (!response.ok) return
52      const result = await response.json()
53      if (this.finished || !this.element.isConnected || document.visibilityState !== "visible") return
54      // A different tab may have switched the signed-in account.
55      if (result.transaction_id !== this.transactionIdValue) return this.finish()
56      if (["dispatched", "legacy", "excluded"].includes(result.status) || !result.tracking_needed) return this.finish()
57      if (result.status !== "pending" || !result.conversion) return
58
59      if (this.delivery.callbackReceived) return await this.acknowledge()
60      if (this.delivery.dispatchAttempts && Date.now() - this.delivery.startedAt < CALLBACK_RETRY_DELAY) return
61      if (typeof window.gtag !== "function") return
62
63      this.delivery.startedAt = Date.now()
64      this.delivery.dispatchAttempts += 1
65      window.gtag("event", "conversion", {
66        ...result.conversion,
67        transport_type: "beacon",
68        event_callback: () => {
69          // This confirms dispatch through the tag, not receipt/attribution
70          // by Google. If acknowledgment fails, retry it on the next check.
71          this.delivery.callbackReceived = true
72          this.acknowledge()
73        }
74      })
75    } catch {
76      // Analytics must never interrupt recording. Reconcile on the next poll.
77    } finally {
78      this.checking = false
79    }
80  }
81
82  async acknowledge() {
83    const delivery = this.delivery
84    if (delivery.acknowledging || delivery.acknowledged) return
85    if (delivery.acknowledgmentAttempts >= MAX_ACKNOWLEDGMENT_ATTEMPTS) return this.finish()
86    delivery.acknowledging = true
87    delivery.acknowledgmentAttempts += 1
88    try {
89      const response = await this.request({
90        method: "POST",
91        headers: {
92          "Content-Type": "application/json",
93          "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content || ""
94        },
95        body: JSON.stringify({ transaction_id: this.transactionIdValue })
96      })
97      if (response.ok) {
98        delivery.acknowledged = true
99        this.finish()
100      }
101    } catch {
102      // Keep callbackReceived so a later poll retries only acknowledgment.
103    } finally {
104      delivery.acknowledging = false
105      if (delivery.acknowledgmentAttempts >= MAX_ACKNOWLEDGMENT_ATTEMPTS) this.finish()
106    }
107  }
108
109  request(options = {}) {
110    return fetch(this.urlValue, {
111      credentials: "same-origin",
112      cache: "no-store",
113      redirect: "error",
114      signal: AbortSignal.timeout(10000),
115      ...options,
116      headers: { Accept: "application/json", ...options.headers }
117    })
118  }
119
120  finish() {
121    this.finished = true
122    clearInterval(this.timer)
123    clearTimeout(this.deadline)
124  }
125}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.