def update_flight(link, campaign, triggered_by=None): """Add/update a reddit campaign as an Adzerk Flight""" if getattr(campaign, 'external_flight_id', None) is not None: az_flight = adzerk_api.Flight.get(campaign.external_flight_id) else: az_flight = None # backwards compatability if campaign.platform == "mobile": campaign.platform = "mobile_web" campaign._commit() campaign_overdelivered = is_overdelivered(campaign) delayed_start = campaign.start_date + datetime.timedelta(minutes=15) if delayed_start >= campaign.end_date: # start time must be before end time delayed_start = campaign.start_date keywords = set(campaign.target.subreddit_names) targets_frontpage = Frontpage.name in keywords if targets_frontpage: keywords.remove(Frontpage.name) # Don't allow nsfw links to show up on the frontpage if link.over_18: keywords.add("!s.frontpage") elif targets_frontpage: keywords.add("s.frontpage") campaign_needs_approval = (not campaign.is_approved and promote.campaign_needs_review(campaign, link)) campaign_is_paused = campaign.paused campaign_needs_payment = not promote.charged_or_not_needed(campaign) campaign_is_terminated = campaign.is_terminated is_active = flight_is_active( needs_approval=campaign_needs_approval, is_paused=campaign_is_paused, needs_payment=campaign_needs_payment, is_terminated=campaign_is_terminated, is_deleted=campaign._deleted, is_overdelivered=campaign_overdelivered, ) d = { 'StartDate': date_to_adzerk(delayed_start), 'EndDate': date_to_adzerk(campaign.end_date), 'OptionType': 1, # 1: CPM, 2: Remainder 'IsUnlimited': False, 'IsFullSpeed': False, 'Keywords': '\n'.join(list(keywords)), 'CampaignId': link.external_campaign_id, 'PriorityId': g.az_selfserve_priorities[campaign.priority_name], 'IsDeleted': False, 'IsActive': is_active, } if campaign.frequency_cap: d.update({'IsFreqCap': True, 'FreqCap': campaign.frequency_cap, 'FreqCapDuration': FREQUENCY_CAP_DURATION_HOURS, 'FreqCapType': FREQ_CAP_TYPE.hour}) else: d['IsFreqCap'] = None if campaign.is_house: # house campaigns are flat rate (free) 100% impression goal d.update({ 'Price': 0, # free/0 revenue 'Impressions': 100, # 100% impressions/rotates with other house campaigns. 'GoalType': 2, # percentage 'RateType': 1, # flat }) else: # price is bid if the rate type is cpm/cpc d.update({ 'Price': campaign.bid_pennies / 100., # convert cents to dollars 'GoalType': GOAL_TYPE_BY_COST_BASIS[campaign.cost_basis], # goal should be based on billing type 'RateType': RATE_TYPE_BY_COST_BASIS[campaign.cost_basis], }) stop_showing_onclick = campaign.cost_basis == promo.PROMOTE_COST_BASIS.cpc d.update({ "BehavioralTargeting": { "onClick": { "stopShowingAdsFromFlight": stop_showing_onclick, }, }, }) if campaign.cost_basis == promo.PROMOTE_COST_BASIS.fixed_cpm: d['Impressions'] = campaign.impressions + ADZERK_IMPRESSION_BUMP else: total_budget_dollars = campaign.total_budget_pennies / 100. daily_cap_dollars = total_budget_dollars / max(campaign.ndays, 1) ndays = campaign.ndays padding = 1 + (1. / (ndays + 1)) lifetime_cap = int(math.ceil(total_budget_dollars)) # budget caps must be whole dollar amounts. round things up to prevent # things from underdelivering. d.update({ 'CapType': 4, 'LifetimeCapAmount': lifetime_cap, }) if not campaign.use_daily_budget_cap: # set the daily cap to the same as the lifetime cap since # `update_changed` doesn't handle unsetting attributes. d['DailyCapAmount'] = lifetime_cap else: d['DailyCapAmount'] = int(math.ceil(daily_cap_dollars * padding)) # Zerkel queries here if campaign.mobile_os: queries_list = [] if 'iOS' in campaign.mobile_os: ios_targets = get_mobile_targeting_query(os_str='iOS', lookup_str='modelName', devices=campaign.ios_devices, versions=campaign.ios_version_range) queries_list.append(ios_targets) if 'Android' in campaign.mobile_os: android_targets = get_mobile_targeting_query(os_str='Android', lookup_str='formFactor', devices=campaign.android_devices, versions=campaign.android_version_range) queries_list.append(android_targets) if campaign.platform == 'all': queries_list.append('($device.formFactor CONTAINS "desktop")') mobile_targeting_query = ' OR '.join(queries_list) d.update({ 'CustomTargeting': mobile_targeting_query, }) else: d.update({ 'CustomTargeting': '', }) if campaign.platform != 'all': site_targeting = [{ 'SiteId': g.az_selfserve_site_ids[campaign.platform], 'IsExclude': False, 'ZoneId': None, }] if az_flight: site_targeting[0]['FlightId'] = az_flight.Id # Check equality more specifically to reduce spam in the PromotionLog. update_site_targeting = True if az_flight and az_flight.SiteZoneTargeting: # Loop through the existing site targeting and remove the `Id` param. old_site_targeting = map( lambda (index, value): { key: value[key] for key in value.keys() if key != "Id"}, enumerate(az_flight.SiteZoneTargeting), ) update_site_targeting = old_site_targeting != site_targeting if update_site_targeting: d.update({ 'SiteZoneTargeting': site_targeting, }) elif az_flight and az_flight.SiteZoneTargeting: d.update({ 'SiteZoneTargeting': [], }) # special handling for location conversions between reddit and adzerk if campaign.location: campaign_country = campaign.location.country campaign_region = campaign.location.region if campaign.location.metro: campaign_metro = int(campaign.location.metro) else: campaign_metro = None if az_flight and az_flight.GeoTargeting: # special handling for geotargeting of existing flights # can't update geotargeting through the Flight endpoint, do it manually existing = az_flight.GeoTargeting[0] az_geotarget = adzerk_api.GeoTargeting._from_item(existing) if (campaign.location and (campaign_country != az_geotarget.CountryCode or campaign_region != az_geotarget.Region or campaign_metro != az_geotarget.MetroCode or az_geotarget.IsExclude)): # existing geotargeting doesn't match current location az_geotarget.CountryCode = campaign_country az_geotarget.Region = campaign_region az_geotarget.MetroCode = campaign_metro az_geotarget.IsExclude = False az_geotarget._send(az_flight.Id) log_text = 'updated geotargeting to %s' % campaign.location PromotionLog.add(link, log_text) elif not campaign.location: # flight should no longer be geotargeted az_geotarget._delete(az_flight.Id) log_text = 'deleted geotargeting' PromotionLog.add(link, log_text) # only allow one geotarget per flight for existing in az_flight.GeoTargeting[1:]: az_geotarget = adzerk_api.GeoTargeting._from_item(existing) az_geotarget._delete(az_flight.Id) # NOTE: need to unset GeoTargeting otherwise it will be added to the # flight again when we _send updates az_flight.GeoTargeting = None elif campaign.location: # flight endpoint works when a new flight is being created or an # existing one that didn't have geotargeting is being updated d.update({ 'GeoTargeting': [{ 'CountryCode': campaign_country, 'Region': campaign_region, 'MetroCode': campaign_metro, 'IsExclude': False, }], }) else: # no geotargeting, either a new flight is being created or an existing # flight is being updated that wasn't geotargeted d.update({ 'GeoTargeting': [], }) request_error = None additional_data = dict( requires_approval=campaign_needs_approval, requires_payment=campaign_needs_payment, is_overdelivered=campaign_overdelivered, is_paused=campaign_is_paused, is_terminated=campaign_is_terminated, ) if az_flight: try: changed = update_changed(az_flight, **d) except adzerk_api.AdzerkError as e: request_error = e finally: g.ad_events.adzerk_api_request( request_type="update_flight", thing=campaign, request_body=d, triggered_by=triggered_by, additional_data=additional_data, request_error=request_error, ) # re-raise after sending event to requeue item. if request_error: raise request_error change_strs = make_change_strings(changed) if campaign_overdelivered: billable = promote.get_billable_impressions(campaign) over_str = 'overdelivered %s/%s' % (billable, campaign.impressions) change_strs.append(over_str) if change_strs: log_text = 'updated %s: ' % az_flight + ', '.join(change_strs) else: log_text = None else: d.update({'Name': campaign._fullname}) try: az_flight = adzerk_api.Flight.create(**d) except adzerk_api.AdzerkError as e: request_error = e finally: g.ad_events.adzerk_api_request( request_type="create_flight", thing=campaign, request_body=d, triggered_by=triggered_by, additional_data=additional_data, request_error=request_error, ) # re-raise after sending event to requeue item. if request_error: raise request_error campaign.external_flight_id = az_flight.Id campaign._commit() PromoCampaignByFlightIdCache.add(campaign) log_text = 'created %s' % az_flight if log_text: PromotionLog.add(link, log_text) g.log.info(log_text) if campaign_overdelivered: campaign.external_flight_overdelivered = True campaign._commit() return az_flight
def adzerk_request( keywords, properties, user_id, num_placements=1, timeout=1.5, platform="desktop", is_refresh=False, referrer=None, ): placements = [] divs = ["div%s" % i for i in xrange(num_placements)] subreddit = None if isinstance(c.site, Subreddit) and not c.default_sr: subreddit = c.site.name for div in divs: placement = { "divName": div, "networkId": g.az_selfserve_network_id, "siteId": g.az_selfserve_site_ids[platform], "adTypes": [LEADERBOARD_AD_TYPE], "eventIds": [EVENT_TYPE_UPVOTE, EVENT_TYPE_DOWNVOTE], "properties": properties, } if subreddit is not None: placement["properties"] = { "subreddit": subreddit, } placements.append(placement) keywords = [word.lower() for word in keywords] data = { "placements": placements, "keywords": keywords, "ip": request.ip, "enableBotFiltering": True, "includePricingData": True, } page_url = request.headers.get("referer", None) if page_url is not None: data["url"] = page_url if referrer is not None: data["referrer"] = referrer if user_id: data["user"] = {"key": user_id} url = 'https://%s/api/v2' % g.adzerk_engine_domain headers = { 'content-type': 'application/json', 'user-agent': request.headers.get('User-Agent'), } do_not_track = request.headers.get("DNT", None) if do_not_track and feature.is_enabled("adzerk_do_not_track"): headers["DNT"] = do_not_track timer = g.stats.get_timer("providers.adzerk") timer.start() for placement in placements: g.ad_events.ad_request( keywords=keywords, platform=platform, placement_name=placement["divName"], placement_types=placement["adTypes"], is_refresh=is_refresh, subreddit=c.site, request=request, context=c, ) try: r = requests.post(url, data=json.dumps(data), headers=headers, timeout=timeout) except (requests.exceptions.Timeout, requests.exceptions.SSLError): g.stats.simple_event('adzerk.request.timeout') return None except requests.exceptions.ConnectionError: g.stats.simple_event('adzerk.request.refused') return None except select.error: return None finally: timer.stop() errored = False try: response = adzerk_api.handle_response(r) except adzerk_api.AdzerkError: g.stats.simple_event('adzerk.request.badresponse') g.log.error('adzerk_request: bad response (%s) %r', r.status_code, r.content) errored = True finally: # Temporarily log request data and response body, # sample at 1% if random.random() < g.live_config.get('ad_log_sample_rate', 0): g.log.info("ad_request [DNT=%s]: %s, ad_response: [%s] %s", do_not_track, json.dumps(data), r.status_code, r.text) if errored: return None decisions = response['decisions'] if not decisions: return None placements_by_div = {placement["divName"]: placement for placement in placements} res = [] for div in divs: decision = decisions[div] if not decision: continue placement = placements_by_div[div] ad_id = decision['adId'] pricing = decision.get("pricing", {}) revenue = pricing.get("revenue") rate_type_id = pricing.get("rateType") rate_type = RATE_TYPE_NAMES.get(rate_type_id, None) impression_url = decision.get("impressionUrl") impression_b64_data = UrlParser(impression_url).query_dict.get("e", "") impression_id, matched_keywords = None, [] try: # fix padding and string encode impression_b64_data = str( impression_b64_data + ("=" * (len(impression_b64_data) % 4)) ) impression_data = json.loads( base64.urlsafe_b64decode(impression_b64_data), strict=False, ) except UnicodeDecodeError: g.log.info("unable to decode impression data: %s", impression_b64_data) impression_data = None except TypeError, ValueError: impression_data = None if impression_data is not None: impression_id = impression_data.get("di") matched_keywords = impression_data.get("mk") if matched_keywords: matched_keywords = matched_keywords.split(",") # adserver ads are not reddit links, we return the body if decision['campaignId'] in g.adserver_campaign_ids: g.ad_events.ad_response( keywords=keywords, platform=platform, placement_name=div, placement_types=placement["adTypes"], ad_id=ad_id, impression_id=impression_id, matched_keywords=matched_keywords, rate_type=rate_type, clearing_price=revenue, subreddit=c.site, request=request, context=c, ) return AdserverResponse(decision['contents'][0]['body']) adzerk_flight_id = decision['flightId'] imp_pixel = decision['impressionUrl'] click_url = decision['clickUrl'] events_by_id = {event["id"]: event["url"] for event in decision["events"]} upvote_pixel = events_by_id[EVENT_TYPE_UPVOTE] downvote_pixel = events_by_id[EVENT_TYPE_DOWNVOTE] campaign_fullname = PromoCampaignByFlightIdCache.get(adzerk_flight_id) contents = decision['contents'][0] body = json.loads(contents['body']) link_fullname = body['link'] target = body['target'] priority = None priority_id = body.get('priorityId', None) ecpm = body.get('ecpm', None) moat_query = body.get('moatQuery', None) if priority_id is not None: try: priority_id = int(priority_id) except ValueError: pass for k, v in g.az_selfserve_priorities.iteritems(): if priority_id == v: priority = k g.ad_events.ad_response( keywords=keywords, platform=platform, placement_name=div, placement_types=placement["adTypes"], ad_id=ad_id, impression_id=impression_id, matched_keywords=matched_keywords, rate_type=rate_type, clearing_price=revenue, subreddit=c.site, link_fullname=link_fullname, campaign_fullname=campaign_fullname, priority=priority, ecpm=ecpm, request=request, context=c, ) if not campaign_fullname: link = Link._by_fullname(link_fullname, data=True, stale=True) if promote.is_external(link): campaign_fullname = promote.EXTERNAL_CAMPAIGN else: adzerk_campaign_id = decision['campaignId'] g.stats.simple_event('adzerk.request.orphaned_flight') g.log.error('adzerk_request: couldn\'t find campaign for flight (az campaign: %s, flight: %s)', adzerk_campaign_id, adzerk_flight_id) # deactivate the flight, it will be reactivated if a # valid campaign actually exists deactivate_orphaned_flight(adzerk_flight_id) continue res.append(AdzerkResponse( link=link_fullname, campaign=campaign_fullname, target=target, ecpm=ecpm, priority=priority, moat_query=moat_query, imp_pixel=imp_pixel, click_url=click_url, upvote_pixel=upvote_pixel, downvote_pixel=downvote_pixel, ))
def update_flight(link, campaign, az_campaign): """Add/update a reddit campaign as an Adzerk Flight""" if getattr(campaign, 'external_flight_id', None) is not None: az_flight = adzerk_api.Flight.get(campaign.external_flight_id) else: az_flight = None campaign_overdelivered = is_overdelivered(campaign) delayed_start = campaign.start_date + datetime.timedelta(minutes=15) if delayed_start >= campaign.end_date: # start time must be before end time delayed_start = campaign.start_date d = { 'StartDate': date_to_adzerk(delayed_start), 'EndDate': date_to_adzerk(campaign.end_date), 'OptionType': 1, # 1: CPM, 2: Remainder 'IsUnlimited': False, 'IsFullSpeed': False, 'Keywords': '\n'.join(campaign.target.subreddit_names), 'CampaignId': az_campaign.Id, 'PriorityId': g.az_selfserve_priorities[campaign.priority_name], 'IsDeleted': False, 'IsActive': (promote.charged_or_not_needed(campaign) and not (campaign._deleted or campaign_overdelivered)), } if campaign.frequency_cap and campaign.frequency_cap_duration: d.update({'IsFreqCap': True, 'FreqCap': campaign.frequency_cap, 'FreqCapDuration': campaign.frequency_cap_duration, 'FreqCapType': FREQ_CAP_TYPE.hour}) else: d['IsFreqCap'] = None is_cpm = hasattr(campaign, 'cpm') and campaign.priority.cpm if is_cpm: d.update({ 'Price': campaign.cpm / 100., # convert from cents to dollars 'Impressions': campaign.impressions + ADZERK_IMPRESSION_BUMP, 'GoalType': 1, # 1: Impressions 'RateType': 2, # 2: CPM }) else: d.update({ 'Price': campaign.bid, 'Impressions': 100, 'GoalType': 2, # 2: Percentage 'RateType': 1, # 1: Flat }) # Zerkel queries here if campaign.mobile_os: queries_list = [] if 'iOS' in campaign.mobile_os: ios_targets = get_mobile_targeting_query(os_str='iOS', lookup_str='modelName', devices=campaign.ios_devices, versions=campaign.ios_version_range) queries_list.append(ios_targets) if 'Android' in campaign.mobile_os: android_targets = get_mobile_targeting_query(os_str='Android', lookup_str='formFactor', devices=campaign.android_devices, versions=campaign.android_version_range) queries_list.append(android_targets) if campaign.platform == 'all': queries_list.append('($device.formFactor CONTAINS "desktop")') mobile_targeting_query = ' OR '.join(queries_list) d.update({ 'CustomTargeting': mobile_targeting_query, }) else: d.update({ 'CustomTargeting': '', }) if campaign.platform != 'all': siteZones = [] if campaign.platform == 'desktop': siteZones.append({ 'SiteId': g.az_selfserve_site_id, 'IsExclude': False, }) elif campaign.platform == 'mobile': siteZones.append({ 'SiteId': g.az_selfserve_mobile_web_site_id, 'IsExclude': False, }) if len(siteZones): d.update({ 'SiteZoneTargeting': siteZones }) # special handling for location conversions between reddit and adzerk if campaign.location: campaign_country = campaign.location.country campaign_region = campaign.location.region if campaign.location.metro: campaign_metro = int(campaign.location.metro) else: campaign_metro = None if az_flight and az_flight.GeoTargeting: # special handling for geotargeting of existing flights # can't update geotargeting through the Flight endpoint, do it manually existing = az_flight.GeoTargeting[0] az_geotarget = adzerk_api.GeoTargeting._from_item(existing) if (campaign.location and (campaign_country != az_geotarget.CountryCode or campaign_region != az_geotarget.Region or campaign_metro != az_geotarget.MetroCode or az_geotarget.IsExclude)): # existing geotargeting doesn't match current location az_geotarget.CountryCode = campaign_country az_geotarget.Region = campaign_region az_geotarget.MetroCode = campaign_metro az_geotarget.IsExclude = False az_geotarget._send(az_flight.Id) log_text = 'updated geotargeting to %s' % campaign.location PromotionLog.add(link, log_text) elif not campaign.location: # flight should no longer be geotargeted az_geotarget._delete(az_flight.Id) log_text = 'deleted geotargeting' PromotionLog.add(link, log_text) # only allow one geotarget per flight for existing in az_flight.GeoTargeting[1:]: az_geotarget = adzerk_api.GeoTargeting._from_item(existing) az_geotarget._delete(az_flight.Id) # NOTE: need to unset GeoTargeting otherwise it will be added to the # flight again when we _send updates az_flight.GeoTargeting = None elif campaign.location: # flight endpoint works when a new flight is being created or an # existing one that didn't have geotargeting is being updated d.update({ 'GeoTargeting': [{ 'CountryCode': campaign_country, 'Region': campaign_region, 'MetroCode': campaign_metro, 'IsExclude': False, }], }) else: # no geotargeting, either a new flight is being created or an existing # flight is being updated that wasn't geotargeted d.update({ 'GeoTargeting': [], }) if az_flight: changed = update_changed(az_flight, **d) change_strs = make_change_strings(changed) if campaign_overdelivered: billable = promote.get_billable_impressions(campaign) over_str = 'overdelivered %s/%s' % (billable, campaign.impressions) change_strs.append(over_str) if change_strs: log_text = 'updated %s: ' % az_flight + ', '.join(change_strs) else: log_text = None else: d.update({'Name': campaign._fullname}) az_flight = adzerk_api.Flight.create(**d) campaign.external_flight_id = az_flight.Id campaign._commit() PromoCampaignByFlightIdCache.add(campaign) log_text = 'created %s' % az_flight if log_text: PromotionLog.add(link, log_text) g.log.info(log_text) if campaign_overdelivered: campaign.external_flight_overdelivered = True campaign._commit() return az_flight
def adzerk_request(keywords, uid, num_placements=1, timeout=1.5, mobile_web=False): placements = [] divs = ["div%s" % i for i in xrange(num_placements)] if mobile_web: site_id = g.az_selfserve_mobile_web_site_id else: site_id = g.az_selfserve_site_id for div in divs: placement = { "divName": div, "networkId": g.az_selfserve_network_id, "siteId": site_id, "adTypes": [LEADERBOARD_AD_TYPE] } placements.append(placement) data = { "placements": placements, "keywords": [word.lower() for word in keywords], "ip": request.ip, } if uid: data["user"] = {"key": uid} url = 'https://engine.adzerk.net/api/v2' headers = { 'content-type': 'application/json', 'user-agent': request.headers.get('User-Agent'), } timer = g.stats.get_timer("providers.adzerk") timer.start() try: r = requests.post(url, data=json.dumps(data), headers=headers, timeout=timeout) except (requests.exceptions.Timeout, requests.exceptions.SSLError): g.stats.simple_event('adzerk.request.timeout') return None except requests.exceptions.ConnectionError: g.stats.simple_event('adzerk.request.refused') return None finally: timer.stop() try: response = adzerk_api.handle_response(r) except adzerk_api.AdzerkError: g.stats.simple_event('adzerk.request.badresponse') g.log.error('adzerk_request: bad response (%s) %r', r.status_code, r.content) return None decisions = response['decisions'] if not decisions: return None res = [] for div in divs: decision = decisions[div] if not decision: continue # adserver ads are not reddit links, we return the body if decision['campaignId'] in g.adserver_campaign_ids: return AdserverResponse(decision['contents'][0]['body']) adzerk_campaign_id = decision['campaignId'] adzerk_flight_id = decision['flightId'] imp_pixel = decision['impressionUrl'] click_url = decision['clickUrl'] campaign = PromoCampaignByFlightIdCache.get(adzerk_flight_id) if not campaign: g.stats.simple_event('adzerk.request.orphaned_flight') g.log.error('adzerk_request: couldn\'t find campaign for flight (az campaign: %s, flight: %s)', adzerk_campaign_id, adzerk_flight_id) # deactivate the flight, it will be reactivated if a # valid campaign actually exists deactivate_orphaned_flight(adzerk_flight_id) continue body = json.loads(decision['contents'][0]['body']) link = body['link'] target = body['target'] res.append(AdzerkResponse(link, campaign, target, imp_pixel, click_url)) return res
def adzerk_request( keywords, properties, user_id, num_placements=1, timeout=1.5, platform="desktop", is_refresh=False, ): placements = [] divs = ["div%s" % i for i in xrange(num_placements)] for div in divs: placement = { "divName": div, "networkId": g.az_selfserve_network_id, "siteId": g.az_selfserve_site_ids[platform], "adTypes": [LEADERBOARD_AD_TYPE], "eventIds": [EVENT_TYPE_UPVOTE, EVENT_TYPE_DOWNVOTE], "properties": properties, } placements.append(placement) keywords = [word.lower() for word in keywords] data = { "placements": placements, "keywords": keywords, "ip": request.ip, "enableBotFiltering": True, } referrer = request.headers.get("referer", None) if referrer: data["referrer"] = referrer if user_id: data["user"] = {"key": user_id} url = 'https://%s/api/v2' % g.adzerk_engine_domain headers = { 'content-type': 'application/json', 'user-agent': request.headers.get('User-Agent'), } do_not_track = request.headers.get("DNT", None) if do_not_track and feature.is_enabled("adzerk_do_not_track"): headers["DNT"] = do_not_track timer = g.stats.get_timer("providers.adzerk") timer.start() for placement in placements: g.ad_events.ad_request( keywords=keywords, platform=platform, placement_name=placement["divName"], placement_types=placement["adTypes"], is_refresh=is_refresh, subreddit=c.site, request=request, context=c, ) try: r = requests.post(url, data=json.dumps(data), headers=headers, timeout=timeout) except (requests.exceptions.Timeout, requests.exceptions.SSLError): g.stats.simple_event('adzerk.request.timeout') return None except requests.exceptions.ConnectionError: g.stats.simple_event('adzerk.request.refused') return None finally: timer.stop() errored = False try: response = adzerk_api.handle_response(r) except adzerk_api.AdzerkError: g.stats.simple_event('adzerk.request.badresponse') g.log.error('adzerk_request: bad response (%s) %r', r.status_code, r.content) errored = True finally: # Temporarily log request data and response body, # sample at 1% if random.random() < g.live_config.get('ad_log_sample_rate', 0): g.log.info("ad_request [DNT=%s]: %s, ad_response: [%s] %s", do_not_track, json.dumps(data), r.status_code, r.text) if errored: return None decisions = response['decisions'] if not decisions: return None placements_by_div = {placement["divName"]: placement for placement in placements} res = [] for div in divs: decision = decisions[div] if not decision: continue placement = placements_by_div[div] ad_id = decision['adId'] # adserver ads are not reddit links, we return the body if decision['campaignId'] in g.adserver_campaign_ids: g.ad_events.ad_response( keywords=keywords, platform=platform, placement_name=div, placement_types=placement["adTypes"], ad_id=ad_id, subreddit=c.site, request=request, context=c, ) return AdserverResponse(decision['contents'][0]['body']) adzerk_flight_id = decision['flightId'] imp_pixel = decision['impressionUrl'] click_url = decision['clickUrl'] events_by_id = {event["id"]: event["url"] for event in decision["events"]} upvote_pixel = events_by_id[EVENT_TYPE_UPVOTE] downvote_pixel = events_by_id[EVENT_TYPE_DOWNVOTE] campaign_fullname = PromoCampaignByFlightIdCache.get(adzerk_flight_id) contents = decision['contents'][0] body = json.loads(contents['body']) link_fullname = body['link'] target = body['target'] priority = None priority_id = body.get('priorityId', None) ecpm = body.get('ecpm', None) if priority_id: priority = PRIORITIES_BY_ID.get(priority_id, "unknown (%s)" % priority_id) g.ad_events.ad_response( keywords=keywords, platform=platform, placement_name=div, placement_types=placement["adTypes"], ad_id=ad_id, subreddit=c.site, link_fullname=link_fullname, campaign_fullname=campaign_fullname, priority=priority, ecpm=ecpm, request=request, context=c, ) if not campaign_fullname: link = Link._by_fullname(link_fullname, data=True, stale=True) if promote.is_external(link): campaign_fullname = promote.EXTERNAL_CAMPAIGN else: adzerk_campaign_id = decision['campaignId'] g.stats.simple_event('adzerk.request.orphaned_flight') g.log.error('adzerk_request: couldn\'t find campaign for flight (az campaign: %s, flight: %s)', adzerk_campaign_id, adzerk_flight_id) # deactivate the flight, it will be reactivated if a # valid campaign actually exists deactivate_orphaned_flight(adzerk_flight_id) continue res.append(AdzerkResponse( link=link_fullname, campaign=campaign_fullname, target=target, ecpm=ecpm, priority=priority, imp_pixel=imp_pixel, click_url=click_url, upvote_pixel=upvote_pixel, downvote_pixel=downvote_pixel, )) return res
def adzerk_request(keywords, uid, num_placements=1, timeout=1.5, platform="desktop"): placements = [] divs = ["div%s" % i for i in xrange(num_placements)] for div in divs: placement = { "divName": div, "networkId": g.az_selfserve_network_id, "siteId": g.az_selfserve_site_ids[platform], "adTypes": [LEADERBOARD_AD_TYPE], "eventIds": [EVENT_TYPE_UPVOTE, EVENT_TYPE_DOWNVOTE], } placements.append(placement) data = { "placements": placements, "keywords": [word.lower() for word in keywords], "ip": request.ip, } referrer = request.headers.get("referer", None) if referrer: data["referrer"] = referrer if uid: data["user"] = {"key": uid} url = 'https://%s/api/v2' % g.adzerk_engine_domain headers = { 'content-type': 'application/json', 'user-agent': request.headers.get('User-Agent'), } do_not_track = request.headers.get("DNT", None) if do_not_track and feature.is_enabled("adzerk_do_not_track"): headers["DNT"] = do_not_track timer = g.stats.get_timer("providers.adzerk") timer.start() try: r = requests.post(url, data=json.dumps(data), headers=headers, timeout=timeout) except (requests.exceptions.Timeout, requests.exceptions.SSLError): g.stats.simple_event('adzerk.request.timeout') return None except requests.exceptions.ConnectionError: g.stats.simple_event('adzerk.request.refused') return None finally: timer.stop() try: response = adzerk_api.handle_response(r) except adzerk_api.AdzerkError: g.stats.simple_event('adzerk.request.badresponse') g.log.error('adzerk_request: bad response (%s) %r', r.status_code, r.content) return None decisions = response['decisions'] if not decisions: return None res = [] for div in divs: decision = decisions[div] if not decision: continue # adserver ads are not reddit links, we return the body if decision['campaignId'] in g.adserver_campaign_ids: return AdserverResponse(decision['contents'][0]['body']) adzerk_campaign_id = decision['campaignId'] adzerk_flight_id = decision['flightId'] imp_pixel = decision['impressionUrl'] click_url = decision['clickUrl'] events_by_id = {event["id"]: event["url"] for event in decision["events"]} upvote_pixel = events_by_id[EVENT_TYPE_UPVOTE] downvote_pixel = events_by_id[EVENT_TYPE_DOWNVOTE] campaign = PromoCampaignByFlightIdCache.get(adzerk_flight_id) if not campaign: g.stats.simple_event('adzerk.request.orphaned_flight') g.log.error('adzerk_request: couldn\'t find campaign for flight (az campaign: %s, flight: %s)', adzerk_campaign_id, adzerk_flight_id) # deactivate the flight, it will be reactivated if a # valid campaign actually exists deactivate_orphaned_flight(adzerk_flight_id) continue body = json.loads(decision['contents'][0]['body']) link = body['link'] target = body['target'] res.append(AdzerkResponse( link=link, campaign=campaign, target=target, imp_pixel=imp_pixel, click_url=click_url, upvote_pixel=upvote_pixel, downvote_pixel=downvote_pixel, )) return res
def update_flight(link, campaign, az_campaign): """Add/update a reddit campaign as an Adzerk Flight""" if getattr(campaign, 'external_flight_id', None) is not None: az_flight = adzerk_api.Flight.get(campaign.external_flight_id) else: az_flight = None # backwards compatability if campaign.platform == "mobile": campaign.platform = "mobile_web" campaign._commit() campaign_overdelivered = is_overdelivered(campaign) delayed_start = campaign.start_date + datetime.timedelta(minutes=15) if delayed_start >= campaign.end_date: # start time must be before end time delayed_start = campaign.start_date keywords = campaign.target.subreddit_names # Don't allow nsfw links to show up on the frontpage if link.over_18: keywords.append('!' + Frontpage.name) d = { 'StartDate': date_to_adzerk(delayed_start), 'EndDate': date_to_adzerk(campaign.end_date), 'OptionType': 1, # 1: CPM, 2: Remainder 'IsUnlimited': False, 'IsFullSpeed': False, 'Keywords': '\n'.join(keywords), 'CampaignId': az_campaign.Id, 'PriorityId': g.az_selfserve_priorities[campaign.priority_name], 'IsDeleted': False, 'IsActive': (not campaign.paused and promote.charged_or_not_needed(campaign) and not (campaign._deleted or campaign_overdelivered)), } if campaign.frequency_cap: d.update({'IsFreqCap': True, 'FreqCap': campaign.frequency_cap, 'FreqCapDuration': FREQUENCY_CAP_DURATION_HOURS, 'FreqCapType': FREQ_CAP_TYPE.hour}) else: d['IsFreqCap'] = None if campaign.is_house: # house campaigns are flat rate (free) 100% impression goal d.update({ 'Price': 0, # free/0 revenue 'Impressions': 100, # 100% impressions/rotates with other house campaigns. 'GoalType': 2, # percentage 'RateType': 1, # flat }) else: # price is bid if the rate type is cpm/cpc d.update({ 'Price': campaign.bid_pennies / 100., # convert cents to dollars 'GoalType': GOAL_TYPE_BY_COST_BASIS[campaign.cost_basis], # goal should be based on billing type 'RateType': RATE_TYPE_BY_COST_BASIS[campaign.cost_basis], }) if campaign.cost_basis == promo.PROMOTE_COST_BASIS.fixed_cpm: d['Impressions'] = campaign.impressions + ADZERK_IMPRESSION_BUMP else: total_budget_dollars = campaign.total_budget_pennies / 100. # budget caps must be whole dollar amounts. round things up to prevent # things from underdelivering. d.update({ 'CapType': 4, 'DailyCapAmount': int(math.ceil(total_budget_dollars / campaign.ndays)), 'LifetimeCapAmount': int(math.ceil(total_budget_dollars)), }) # Zerkel queries here if campaign.mobile_os: queries_list = [] if 'iOS' in campaign.mobile_os: ios_targets = get_mobile_targeting_query(os_str='iOS', lookup_str='modelName', devices=campaign.ios_devices, versions=campaign.ios_version_range) queries_list.append(ios_targets) if 'Android' in campaign.mobile_os: android_targets = get_mobile_targeting_query(os_str='Android', lookup_str='formFactor', devices=campaign.android_devices, versions=campaign.android_version_range) queries_list.append(android_targets) if campaign.platform == 'all': queries_list.append('($device.formFactor CONTAINS "desktop")') mobile_targeting_query = ' OR '.join(queries_list) d.update({ 'CustomTargeting': mobile_targeting_query, }) else: d.update({ 'CustomTargeting': '', }) if campaign.platform != 'all': d.update({ 'SiteZoneTargeting': [{ 'SiteId': g.az_selfserve_site_ids[campaign.platform], 'IsExclude': False, }], }) # special handling for location conversions between reddit and adzerk if campaign.location: campaign_country = campaign.location.country campaign_region = campaign.location.region if campaign.location.metro: campaign_metro = int(campaign.location.metro) else: campaign_metro = None if az_flight and az_flight.GeoTargeting: # special handling for geotargeting of existing flights # can't update geotargeting through the Flight endpoint, do it manually existing = az_flight.GeoTargeting[0] az_geotarget = adzerk_api.GeoTargeting._from_item(existing) if (campaign.location and (campaign_country != az_geotarget.CountryCode or campaign_region != az_geotarget.Region or campaign_metro != az_geotarget.MetroCode or az_geotarget.IsExclude)): # existing geotargeting doesn't match current location az_geotarget.CountryCode = campaign_country az_geotarget.Region = campaign_region az_geotarget.MetroCode = campaign_metro az_geotarget.IsExclude = False az_geotarget._send(az_flight.Id) log_text = 'updated geotargeting to %s' % campaign.location PromotionLog.add(link, log_text) elif not campaign.location: # flight should no longer be geotargeted az_geotarget._delete(az_flight.Id) log_text = 'deleted geotargeting' PromotionLog.add(link, log_text) # only allow one geotarget per flight for existing in az_flight.GeoTargeting[1:]: az_geotarget = adzerk_api.GeoTargeting._from_item(existing) az_geotarget._delete(az_flight.Id) # NOTE: need to unset GeoTargeting otherwise it will be added to the # flight again when we _send updates az_flight.GeoTargeting = None elif campaign.location: # flight endpoint works when a new flight is being created or an # existing one that didn't have geotargeting is being updated d.update({ 'GeoTargeting': [{ 'CountryCode': campaign_country, 'Region': campaign_region, 'MetroCode': campaign_metro, 'IsExclude': False, }], }) else: # no geotargeting, either a new flight is being created or an existing # flight is being updated that wasn't geotargeted d.update({ 'GeoTargeting': [], }) if az_flight: changed = update_changed(az_flight, **d) change_strs = make_change_strings(changed) if campaign_overdelivered: billable = promote.get_billable_impressions(campaign) over_str = 'overdelivered %s/%s' % (billable, campaign.impressions) change_strs.append(over_str) if change_strs: log_text = 'updated %s: ' % az_flight + ', '.join(change_strs) else: log_text = None else: d.update({'Name': campaign._fullname}) az_flight = adzerk_api.Flight.create(**d) campaign.external_flight_id = az_flight.Id campaign._commit() PromoCampaignByFlightIdCache.add(campaign) log_text = 'created %s' % az_flight if log_text: PromotionLog.add(link, log_text) g.log.info(log_text) if campaign_overdelivered: campaign.external_flight_overdelivered = True campaign._commit() return az_flight
def adzerk_request(keywords, uid, num_placements=1, timeout=1.5, platform="desktop"): placements = [] divs = ["div%s" % i for i in xrange(num_placements)] for div in divs: placement = { "divName": div, "networkId": g.az_selfserve_network_id, "siteId": g.az_selfserve_site_ids[platform], "adTypes": [LEADERBOARD_AD_TYPE], "eventIds": [EVENT_TYPE_UPVOTE, EVENT_TYPE_DOWNVOTE], } placements.append(placement) data = { "placements": placements, "keywords": [word.lower() for word in keywords], "ip": request.ip, } referrer = request.headers.get("referer", None) if referrer: data["referrer"] = referrer if uid: data["user"] = {"key": uid} url = 'https://%s/api/v2' % g.adzerk_engine_domain headers = { 'content-type': 'application/json', 'user-agent': request.headers.get('User-Agent'), } do_not_track = request.headers.get("DNT", None) if do_not_track and feature.is_enabled("adzerk_do_not_track"): headers["DNT"] = do_not_track timer = g.stats.get_timer("providers.adzerk") timer.start() try: r = requests.post(url, data=json.dumps(data), headers=headers, timeout=timeout) except (requests.exceptions.Timeout, requests.exceptions.SSLError): g.stats.simple_event('adzerk.request.timeout') return None except requests.exceptions.ConnectionError: g.stats.simple_event('adzerk.request.refused') return None finally: timer.stop() try: response = adzerk_api.handle_response(r) except adzerk_api.AdzerkError: g.stats.simple_event('adzerk.request.badresponse') g.log.error('adzerk_request: bad response (%s) %r', r.status_code, r.content) return None decisions = response['decisions'] if not decisions: return None res = [] for div in divs: decision = decisions[div] if not decision: continue # adserver ads are not reddit links, we return the body if decision['campaignId'] in g.adserver_campaign_ids: return AdserverResponse(decision['contents'][0]['body']) adzerk_campaign_id = decision['campaignId'] adzerk_flight_id = decision['flightId'] imp_pixel = decision['impressionUrl'] click_url = decision['clickUrl'] events_by_id = { event["id"]: event["url"] for event in decision["events"] } upvote_pixel = events_by_id[EVENT_TYPE_UPVOTE] downvote_pixel = events_by_id[EVENT_TYPE_DOWNVOTE] campaign = PromoCampaignByFlightIdCache.get(adzerk_flight_id) if not campaign: g.stats.simple_event('adzerk.request.orphaned_flight') g.log.error( 'adzerk_request: couldn\'t find campaign for flight (az campaign: %s, flight: %s)', adzerk_campaign_id, adzerk_flight_id) # deactivate the flight, it will be reactivated if a # valid campaign actually exists deactivate_orphaned_flight(adzerk_flight_id) continue body = json.loads(decision['contents'][0]['body']) link = body['link'] target = body['target'] res.append( AdzerkResponse( link=link, campaign=campaign, target=target, imp_pixel=imp_pixel, click_url=click_url, upvote_pixel=upvote_pixel, downvote_pixel=downvote_pixel, )) return res
def update_flight(link, campaign, az_campaign): """Add/update a reddit campaign as an Adzerk Flight""" if getattr(campaign, 'external_flight_id', None) is not None: az_flight = adzerk_api.Flight.get(campaign.external_flight_id) else: az_flight = None # backwards compatability if campaign.platform == "mobile": campaign.platform = "mobile_web" campaign._commit() campaign_overdelivered = is_overdelivered(campaign) delayed_start = campaign.start_date + datetime.timedelta(minutes=15) if delayed_start >= campaign.end_date: # start time must be before end time delayed_start = campaign.start_date keywords = campaign.target.subreddit_names # Don't allow nsfw links to show up on the frontpage if link.over_18: keywords.append('!' + Frontpage.name) d = { 'StartDate': date_to_adzerk(delayed_start), 'EndDate': date_to_adzerk(campaign.end_date), 'OptionType': 1, # 1: CPM, 2: Remainder 'IsUnlimited': False, 'IsFullSpeed': False, 'Keywords': '\n'.join(keywords), 'CampaignId': az_campaign.Id, 'PriorityId': g.az_selfserve_priorities[campaign.priority_name], 'IsDeleted': False, 'IsActive': (not campaign.paused and promote.charged_or_not_needed(campaign) and not (campaign._deleted or campaign_overdelivered)), } if campaign.frequency_cap: d.update({ 'IsFreqCap': True, 'FreqCap': campaign.frequency_cap, 'FreqCapDuration': FREQUENCY_CAP_DURATION_HOURS, 'FreqCapType': FREQ_CAP_TYPE.hour }) else: d['IsFreqCap'] = None if campaign.is_house: # house campaigns are flat rate (free) 100% impression goal d.update({ 'Price': 0, # free/0 revenue 'Impressions': 100, # 100% impressions/rotates with other house campaigns. 'GoalType': 2, # percentage 'RateType': 1, # flat }) else: # price is bid if the rate type is cpm/cpc d.update({ 'Price': campaign.bid_pennies / 100., # convert cents to dollars 'GoalType': GOAL_TYPE_BY_COST_BASIS[ campaign.cost_basis], # goal should be based on billing type 'RateType': RATE_TYPE_BY_COST_BASIS[campaign.cost_basis], }) if campaign.cost_basis == promo.PROMOTE_COST_BASIS.fixed_cpm: d['Impressions'] = campaign.impressions + ADZERK_IMPRESSION_BUMP else: total_budget_dollars = campaign.total_budget_pennies / 100. # budget caps must be whole dollar amounts. round things up to prevent # things from underdelivering. d.update({ 'CapType': 4, 'DailyCapAmount': int(math.ceil(total_budget_dollars / campaign.ndays)), 'LifetimeCapAmount': int(math.ceil(total_budget_dollars)), }) # Zerkel queries here if campaign.mobile_os: queries_list = [] if 'iOS' in campaign.mobile_os: ios_targets = get_mobile_targeting_query( os_str='iOS', lookup_str='modelName', devices=campaign.ios_devices, versions=campaign.ios_version_range) queries_list.append(ios_targets) if 'Android' in campaign.mobile_os: android_targets = get_mobile_targeting_query( os_str='Android', lookup_str='formFactor', devices=campaign.android_devices, versions=campaign.android_version_range) queries_list.append(android_targets) if campaign.platform == 'all': queries_list.append('($device.formFactor CONTAINS "desktop")') mobile_targeting_query = ' OR '.join(queries_list) d.update({ 'CustomTargeting': mobile_targeting_query, }) else: d.update({ 'CustomTargeting': '', }) if campaign.platform != 'all': d.update({ 'SiteZoneTargeting': [{ 'SiteId': g.az_selfserve_site_ids[campaign.platform], 'IsExclude': False, }], }) # special handling for location conversions between reddit and adzerk if campaign.location: campaign_country = campaign.location.country campaign_region = campaign.location.region if campaign.location.metro: campaign_metro = int(campaign.location.metro) else: campaign_metro = None if az_flight and az_flight.GeoTargeting: # special handling for geotargeting of existing flights # can't update geotargeting through the Flight endpoint, do it manually existing = az_flight.GeoTargeting[0] az_geotarget = adzerk_api.GeoTargeting._from_item(existing) if (campaign.location and (campaign_country != az_geotarget.CountryCode or campaign_region != az_geotarget.Region or campaign_metro != az_geotarget.MetroCode or az_geotarget.IsExclude)): # existing geotargeting doesn't match current location az_geotarget.CountryCode = campaign_country az_geotarget.Region = campaign_region az_geotarget.MetroCode = campaign_metro az_geotarget.IsExclude = False az_geotarget._send(az_flight.Id) log_text = 'updated geotargeting to %s' % campaign.location PromotionLog.add(link, log_text) elif not campaign.location: # flight should no longer be geotargeted az_geotarget._delete(az_flight.Id) log_text = 'deleted geotargeting' PromotionLog.add(link, log_text) # only allow one geotarget per flight for existing in az_flight.GeoTargeting[1:]: az_geotarget = adzerk_api.GeoTargeting._from_item(existing) az_geotarget._delete(az_flight.Id) # NOTE: need to unset GeoTargeting otherwise it will be added to the # flight again when we _send updates az_flight.GeoTargeting = None elif campaign.location: # flight endpoint works when a new flight is being created or an # existing one that didn't have geotargeting is being updated d.update({ 'GeoTargeting': [{ 'CountryCode': campaign_country, 'Region': campaign_region, 'MetroCode': campaign_metro, 'IsExclude': False, }], }) else: # no geotargeting, either a new flight is being created or an existing # flight is being updated that wasn't geotargeted d.update({ 'GeoTargeting': [], }) if az_flight: changed = update_changed(az_flight, **d) change_strs = make_change_strings(changed) if campaign_overdelivered: billable = promote.get_billable_impressions(campaign) over_str = 'overdelivered %s/%s' % (billable, campaign.impressions) change_strs.append(over_str) if change_strs: log_text = 'updated %s: ' % az_flight + ', '.join(change_strs) else: log_text = None else: d.update({'Name': campaign._fullname}) az_flight = adzerk_api.Flight.create(**d) campaign.external_flight_id = az_flight.Id campaign._commit() PromoCampaignByFlightIdCache.add(campaign) log_text = 'created %s' % az_flight if log_text: PromotionLog.add(link, log_text) g.log.info(log_text) if campaign_overdelivered: campaign.external_flight_overdelivered = True campaign._commit() return az_flight