{"record":{"id":"d3a76e57de86ae86","repo":"antiwork/gumroad","slug":"only-one-analytics-block-is-allowed-per-product-p","errorCode":null,"errorMessage":"Only one analytics block is allowed per product. Please consolidate your tracking segments by pasting them into in a single block.","messagePattern":"Only one analytics block is allowed per product\\. Please consolidate your tracking segments by pasting them into in a single block\\.","errorType":"exception","errorClass":"ThirdPartyAnalytic::ThirdPartyAnalyticInvalid","httpStatus":null,"severity":"error","filePath":"app/models/third_party_analytic.rb","lineNumber":34,"sourceCode":"  FOR_ALL_PRODUCTS = \"#all_products\"\n  LOCATIONS = [\"all\", \"product\", \"receipt\"]\n\n  class ThirdPartyAnalyticInvalid < StandardError\n  end\n\n  def self.save_third_party_analytics(analytics_params, seller)\n    existing_analytics = seller.third_party_analytics.alive\n    keep_analytics = []\n\n    if analytics_params.empty?\n      existing_analytics.each(&:mark_deleted)\n      return []\n    end\n\n    product_hash = Hash.new { |hash, key| hash[key] = {} }\n    analytics_params.each do |third_party_analytic|\n      product = product_from_permalink(third_party_analytic[:product])\n      raise ThirdPartyAnalyticInvalid, \"Only one analytics block is allowed per product. Please consolidate your tracking segments by pasting them into in a single block.\" if product_hash[third_party_analytic[:location]][product.try(:id)]\n\n      product_hash[third_party_analytic[:location]][product.try(:id)] = true\n    end\n\n    analytics_params.each do |third_party_analytic|\n      product = product_from_permalink(third_party_analytic[:product])\n\n      if third_party_analytic[:id].present?\n        analytics = seller.third_party_analytics.alive.find_by_external_id(third_party_analytic[:id])\n        next if analytics.nil?\n\n        analytics.link = product if analytics.link != product\n        analytics.name = third_party_analytic[:name] if third_party_analytic[:name].present?\n        analytics.location = third_party_analytic[:location] if third_party_analytic[:location].present?\n        analytics.analytics_code = third_party_analytic[:code] if analytics.analytics_code != third_party_analytic[:code]\n        analytics.save!\n      else\n        analytics = seller.third_party_analytics.create!(analytics_code: third_party_analytic[:code], name: third_party_analytic[:name], location: third_party_analytic[:location], link: product)","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/third_party_analytic.rb#L16-L52","documentation":"Raised as ThirdPartyAnalyticInvalid (app/models/third_party_analytic.rb:19) by ThirdPartyAnalytic.save_third_party_analytics when the submitted params contain two analytics entries with the same :location and the same product — the method tracks (location, product_id) pairs in product_hash and refuses the second. One analytics block per product per location (e.g. one for the product page, one for checkout) is the invariant, since each block is injected as a single segment in the page.","triggerScenarios":"POST analytics settings with analytics_params where two entries share both :location and :product (same permalink resolving to the same link) — e.g. duplicate rows appended by the settings form, or per-product blocks duplicated when a form state is applied twice.","commonSituations":"Analytics settings UI letting the same product+location row be added twice; integrations syncing tracking blocks that append instead of replace; product_from_permalink resolving two different permalink strings to the same product.","solutions":["Dedupe analytics_params by [location, product] client-side before submitting; keep last-write-wins.","Prevent adding a second block for the same product+location in the settings UI.","Server-side, group params with params.group_by { |a| [a[:location], a[:product]] } and reject/merge duplicates before calling save_third_party_analytics.","Rescue ThirdPartyAnalyticInvalid and show its message."],"exampleFix":"# before\nThirdPartyAnalytic.save_third_party_analytics(analytics_params, seller) # ThirdPartyAnalyticInvalid\n\n# after: last block per (location, product) wins\nnormalized = analytics_params.uniq { |a| [a[:location], a[:product]] }\nThirdPartyAnalytic.save_third_party_analytics(normalized, seller)","handlingStrategy":"validation","validationCode":"keys = analytics_params.map { |a| [a[:location], a[:product]] }\nreturn { error: 'One block per product+location.' } if keys.uniq.size != keys.size","typeGuard":null,"tryCatchPattern":"begin\n  ThirdPartyAnalytic.save_third_party_analytics(analytics_params, seller)\nrescue ThirdPartyAnalytic::ThirdPartyAnalyticInvalid => e\n  render json: { message: e.message }, status: :unprocessable_entity\nend","preventionTips":["Dedupe by [location, product] before submit; merge tracking segments into one block.","Settings UI must replace, not append, a product's block for a given location.","Remember different permalink strings can resolve (product_from_permalink) to the same product — dedupe on resolved product ids."],"tags":["third-party-analytics","analytics","duplicate","settings","rails","validation"],"backgroundTag":"duplicate-resource-per-parent","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}