Firehose

Firehose is an event delivery mechanism introduced as a premium feature for high-volume HasOffers clients, allowing push delivery of tracking and adjustment events to external event consumers. This feature requires you to have an Amazon Web Services (AWS) account as well as have or are a developer available to implement consuming messages from either Amazon's Simple Queue Service (SQS) or Amazon's Kinesis Stream.

This document covers setting your event receiver up, the structure of the overall message you receive, the structure of the event data packaged in each message, types of events, and handling event deduplication. It includes references to the full list of Firehose fields and a basic example consumer.

If you are interested in adding Firehose to your HasOffers account, contact your account manager.

Using SQS

Setting Your SQS Queue Up

To receive a message from TUNE's AWS account to yours, you must give TUNE's account privileges on your SQS queue.

  1. In your AWS console, click on the Permissions tab, then add an additional permission.
  2. Set the Principal to: arn:aws:iam::875314598127:user/ho-firehose-prod
  3. From the Actions dropdown list, check SendMessages. Check only SendMessages, as we don't need to perform any other actions on your queue.
  4. Supply your Queue URL for TUNE.

For documentation from Amazon on setting your queue up, read:
https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/acp-overview.html

Once you've completed the above steps, supply the queue URL to TUNE so we can deliver messages to you. For more information from Amazon on queue URLs, read:
https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/ImportantIdentifiers.html

Message Structure

Firehose message bodies are JSON encapsulations of tracking and adjustment events. Each queue message is a JSON object containing various control fields and a list of of the events as individual JSON objects. (The message itself is also known as an "envelope".)

The message body is gzipped and then base64-encoded before delivery. Your consumer must decode it or pass the encoded body to Amazon's boto library, then unzip the message to get the resulting JSON. The envelope takes this form:

{"dispatch_id":ID,
"dispatch_timestamp":TIMESTAMP,
"is_test":false,
"network_id":NETWORK,
"target_class":"stats",
"dispatch_class":"event",
"data":OBJECT }
  • dispatch_id: ID of the message envelope, such as "de305d54-75b4-431b-adb2-eb6b9e546014". Do not use this for event deduplication. See Handling Message Deduplication below.
  • dispatch_timestamp: time the event batch was created, such as "14395106601".
  • is_test: an internal control field. Ignore this field.
  • network_id: your network ID.
  • target_class: always "stats". Future versions may extend this field to other values.
  • dispatch_class: always "event". Future versions may extend this field to other values.
  • data: JSON object containing a list of one or more tracking or adjustment events, as detailed in the following section.

Event Structure

Each message's data field contains a list of one or more event objects. Each event object contains two identifying fields—event_id and action—and a number of additional fields pertaining to that event. The data field in the envelope takes this form:

"data":{
{"event_id":ID,
"action":ACTION,
OTHER FIELDS }
}
  • event_id: unique ID for that event, such as "de305d54-75b4-431b-adb2-eb6b9e546014". Use this for deduplication.
  • action: type of action for that event: "impression", "click", or "conversion".

For all potential fields, see our Firehose fields document. Note that there are two tabs at the bottom of the page. The first tab at the bottom covers tracking events (impressions, clicks, and conversions). The second tab covers adjustment events.

Click here for the Firehose fields document

Event Types

There are two types of events: standard events (a.k.a. tracking events) and adjustment events.

Determining Event Type

To determine if an event is a standard event or an adjustment event, check for the presence of an adjust_action field. If so, the event is an adjustment. If not, the event is standard.

From there, use the action field to determine if the event is for an impression, click, or conversion.

Standard Events

Standard events cover the raw tracking events from our ad servers: impressions, clicks, and conversions. These come from your raw, unadjusted stats. You can see these events in the application through the event tracer and stat reports.

JSON objects for standard events contain all applicable fields for the event. Certain fields, if they do not apply specifically to that event will be excluded. See Event Structure above for a link to all potential fields.

Adjustment Events

Adjustment events cover the difference applied to your stats, such as rejecting conversion.

JSON objects for adjustment events contain fields relating to the adjustments amounts along with other information that affect stats aggregation where applicable such as affiliate sub IDs and advertiser sub IDs. See Event Structure above for a link to all potential fields.

For example, if you reject a conversion that has a payout of $10 and revenue of $20, the adjustment event data includes the following fields:

"action":"conversion",
"num":-1,
"payout":-10,
"revenue":-20

Adjustment events for impressions and clicks are handled in a similar fashion, with the action set to "impression" or "click".

Adjustment Events Stream

The adjustment events stream is a combination of:

The Adjustment Events stream needs to be included to properly reconcile statistics and update existing conversions with Firehose.

How to Reconcile the Events Stream with the Adjustment Events Stream

First, the adjust_action value will need to be examined in order to determine the proper Adjustment Event action approach.

If adjust_action = 1 (create):  Append the adjustment event to the regular event stream

If adjust_action = 2 (update): These events serve as a ledger of changes to the original event. The event identified via the TUNE Event ID value in the adjustment record will need to be applied to the event record with the same TUNE Event ID value. The TUNE Event ID serves as our primary/unique key for all event records.

Example adjustment outputs:

Creating new Conversions via API

{""action"": ""conversion"",
 ""ad_campaign_creative_id"": 0,
 ""ad_campaign_id"": 0,
 ""adjust_action"": ""create"",
 ""adjustment_sequence_id"": 16648208123366,
 ""adjustment_timestamp"": 1664820812,
 ""adv_sub"": """",
 ""adv_unique1"": """", 
 ""adv_unique2"": """", 
 ""adv_unique3"": """", 
 ""adv_unique4"": """",
 ""adv_unique5"": """",
 ""advertiser_id"": 0,
 ""advertiser_manager_id"": ""0"",
 ""aff_sub"": """",
 ""aff_sub2"": """",
 ""aff_sub3"": """",
 ""aff_sub4"": """",
 ""aff_sub5"": """",
 ""affiliate_click_id"": """",
 ""affiliate_id"": 1029,
 ""affiliate_manager_id"": 1,
 ""affiliate_unique1"": """",
 ""affiliate_unique2"": """",
 ""affiliate_unique3"": """",
 ""affiliate_unique4"": """",
 ""affiliate_unique5"": """",
 ""app_version"": """",
 ""browser_id"": 0,
 ""conversion_id"": 923242,
 ""country_code"": ""INT"",
 ""created_from_adjustment"": true,
 ""created_timestamp"": 1664810032,
 ""creative_url_id"": 0,
 ""currency"": ""EUR"",
 ""current_payout"": 1.0,
 ""current_revenue"": 2.0,
 ""customer_id"": 0,
 ""date"": ""2022-10-03"",
 ""datetime"": ""2022-10-03 11:13:52"",
 ""event_id"": ""a6f0653c-ace9-4e63-bfaf-8c953d30fb99"",
 ""goal_id"": 0,
 ""hour"": 11,
 ""ip"": """",
 ""network_id"": ""demo"",
 ""num"": 1,
 ""offer_file_id"": 0,
 ""offer_id"": 1,
 ""order_id"": """",
 ""payout"": 1.0,
 ""payout_cents"": 1.0,
 ""payout_type"": ""cpa_flat"",
 ""pixel_refer"": """",
 ""product_category"": """",
 ""promo_code"": """",
 ""refer"": """",
 ""revenue"": 2.0,
 ""revenue_cents"": 2.0,
 ""revenue_type"": ""cpa_flat"",
 ""sale_amount"": 0.0,
 ""sale_cents"": 0.0,
 ""session_datetime"": """",
 ""session_ip"": """",
 ""sku_id"": """",
 ""source"": """",
 ""status"": ""approved"",
 ""status_code"": 52, 
 ""timezone"": ""America/New_York"", 
 ""transaction_id"": """",
 ""tune_event_id"": ""adc6c18f-5d50-4a0f-a8cc-ff63f06b6336"",
 ""update_stats"": true, 
 ""user_agent"": """"}

Updating an existing Conversion

{
   """action""":"""conversion""",
   """ad_campaign_creative_id""":0,
   """ad_campaign_id""":0,
   """adjust_action""":"""update""",
   """adjustment_sequence_id""":16648209853209,
   """adjustment_timestamp""":1664820985,
   """adv_sub""":"""",
   """advertiser_id""":0,
   """advertiser_manager_id""":""1"",
   """aff_sub""":"""",
   ""aff_sub2"":"""",
   ""aff_sub3"":"""",
   ""aff_sub4"":"""",
   ""aff_sub5"":"""",
   """affiliate_id""":1029,
   """affiliate_manager_id""":0,
   """browser_id""":7,
   """conversion_id""":329536393,
   """country_code""":"""UK""",
   """created_from_adjustment""":false,
   """created_timestamp""":1664820662,
   """creative_url_id""":0,
   """currency""":"""USD""",
   """current_payout""":7.0,
   """current_revenue""":0.0,
   """current_sale_amount""":0.0,
   """customer_id""":0,
   """date""":""2022-10-03"",
   """datetime""":"""2022-10-03 14":"11":02"",
   """event_id""":""e9cf2e32-3024-46a1-a089-bb56967f83aa"",
   """goal_id""":0,
   """hour""":14,
   """ip""":""10.1.1.10"",
   """network_id""":"""demo""",
   """offer_file_id""":0,
   """offer_id""":1,
   """payout""":0.0,
   """payout_cents""":0.0,
   """payout_type""":"""cpa_flat""",
   """pixel_refer""":"""https":"",
   """promo_code""":"""",
   """refer""":"""",
   """revenue""":0.0,
   """revenue_cents""":0.0,
   """revenue_type""":"""cpa_flat""",
   """sale_amount""":0.0,
   """sale_cents""":0.0,
   """session_datetime""":"""2022-10-03 14":"08":34"",
   """session_ip""":""10.1.1.2"",
   """source""":"""",
   """status""":"""rejected""",
   """status_code""":12,
   """timezone""":"""America/New_York""",
   """transaction_id""":""102dab988204e09a8a6fd048f5c480"",
   """tune_event_id""":""455226b0-e485-4284-9853-4c152dbdece6"",
   """update_stats""":false,
   """user_agent""":""Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML,
   like Gecko) Mobile/15E148""
}

Field Updates

Updates to existing events allow a subset of fields to be modified. These fields will need to either be updated on the original event record or a window function must be used to take the latest adjustment_sequence_id field. The value of the adjustment_sequence_id field can also order the adjustment ledger for each TUNE Event ID.

*The TUNE Event ID is the unique conversion identifier used by TUNE. This ID will need to be included along with the adjustment stream to update existing conversions in the 3rd party destination location.

Handling Event Deduplication

Each tracking event has an alphanumeric identifier, event_id. Use this value for deduplication purposes. Do not use the dispatch_id in the message envelope for deduplication.

The value of event_id is guaranteed to be unique only within a given network. If developing code to work with multiple networks, you must validate against network_id in conjunction with event_id.

Basic Example Consumer

You need a mechanism to consume messages once they start flowing into your SQS queue. See our example message consumer, written in Python and linked to below, for a basic sense of structure.

Our example consumer reads one event at a time from a message and passes that event to a method called write. If you use this example to create a prototype, place the code for processing events in the write method contained within. The code you place is dependent on your system and storage mechanisms, which are outside the purview of HasOffers.

Disclaimer: This code is meant only as an example. It may not be the most expedient way to consume events from the queue depending on your architecture.

Click here for the sample SQS consumer code

Using Kinesis

Kinesis Streams is Amazon’s streaming data service. It can be used to capture and analyze terabytes of data from many sources. More information is available at aws.amazon.com/kinesis/streams.

Setting Your Kinesis Stream Up

First, you need to provide your Kinesis Stream Name and AWS region to your HasOffers account manager or sales engineer, so Firehose is allowed to put records into your Kinesis stream. You also need to request the External ID from us for the next step.

Once we have that information and you have the External ID, you can set Firehose up on your end. Create an IAM role for our AWS account. Here is a document from AWS on how to set up a cross-account IAM policy. Your rule will differ slightly from the document as you will be granting a specific managed policy.

The AWS account_id you need to grant access to is: 875314598127. Our specific production user ARN is: arn:aws:iam::875314598127:user/ho-firehose-prod. Include the External ID provided by us.

You will need to grant the following permissions in this IAM role.

Producer:

Actions Resource Purpose
DescribeStream Amazon Kinesis stream Before attempting to write records, the producer should check if the stream exists and is active
PutRecord, PutRecords Amazon Kinesis stream Write records to Streams

When these steps are completed, the roles permissions should look something like this:

{
  "Version": "2016-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "firehose.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "abc-123"
        }
      }
    }
  ]
}

Message Structure

Each message delivered via Kinesis is a single, direct event, rather than the event/envelope structure used by our SQS sender. This means the messages from Kinesis take on the structure of an event, but include the network's ID (as network_id) as part of the data sent.

Deduplication is still done via the event_id field, as with events delivered via SQS. Events are passed as unnamed JSON objects, as shown in the below examples:

Example Click Message

{"action":"click",
"aff_sub":"trafgen",
"affiliate_id":1039,
"affiliate_manager_id":18,
"click_url":"https://NETWORKID.go2cloud.org/aff_c?offer_id=180\u0026aff_id=1039\u0026source=gwmTrafficGen\u0026aff_sub=trafgen\u0026format=json",
"country_code":"US",
"currency":"EUR",
"datetime":1476127152,
"event_id":"6d631f8e-8f1e-11e6-9a2e-067e12251fb6",
"ip":"207.66.184.66","
is_click_unique":true,
"mobile_carrier":"?",
"network_id":"NETWORKID",
"offer_expires":2419200,
"offer_id":180,
"payout_group_id":0,
"payout_type":"cpa_flat",
"req_connection_speed":"broadband",
"revenue_group_id":0,
"revenue_type":"cpa_flat",
"session_on_impression":false,
"source":"gwmTrafficGen",
"status_code":0,
"timezone":"America/Los_Angeles",
"transaction_id":"10260c5c985c562d0da427bd82b31b",
"user_agent":"Go-http-client/1.1"}

Example Conversion Message

{"action":"conversion",
"aff_sub":"trafgen",
"affiliate_id":1031,
"affiliate_manager_id":18,
"click_url":"https://NETWORKID.go2cloud.org/aff_lsr?offer_id=6\u0026transaction_id=102231e315e61e7c91c48f8561e4fc",
"country_code":"US",
"currency":"EUR",
"datetime":1476127367,
"event_id":"ed963ce0-8f1e-11e6-8104-06b5fd18d4be",
"ip":"207.66.184.66",
"is_click_unique":false,
"mobile_carrier":"?",
"network_id":"NETWORKID",
"offer_expires":1505157760,
"offer_id":6,"payout":0.50000000,
"payout_cents":50.00000000,
"payout_group_id":0,
"payout_type":"cpa_flat",
"req_connection_speed":"broadband",
"revenue":1.00000000,
"revenue_cents":100.00000000,
"revenue_group_id":0,
"revenue_type":"cpa_flat",
"session_datetime":1476127357,
"session_ip":"207.66.184.66",
"session_on_impression":false,
"source":"gwmTrafficGen",
"status":"approved",
"status_code":43,
"timezone":"America/Los_Angeles",
"transaction_id":"102231e315e61e7c91c48f8561e4fc",
"user_agent":"Go-http-client/1.1"}
Have a Question? Please contact [email protected] for technical support.