-
Notifications
You must be signed in to change notification settings - Fork 184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Event endpoint to handle notifications was implemented #296
Changes from 1 commit
949065d
d4fc771
4c27649
39bc5e2
26d5ba6
ac14263
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.prebid.server.analytics.model; | ||
|
||
import lombok.Value; | ||
|
||
@Value | ||
public class NotificationEvent { | ||
String type; | ||
|
||
String bidId; | ||
|
||
String bidder; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package org.prebid.server.handler; | ||
|
||
import io.netty.handler.codec.http.HttpResponseStatus; | ||
import io.vertx.core.Handler; | ||
import io.vertx.core.buffer.Buffer; | ||
import io.vertx.core.http.HttpHeaders; | ||
import io.vertx.core.json.DecodeException; | ||
import io.vertx.core.json.Json; | ||
import io.vertx.ext.web.RoutingContext; | ||
import org.apache.commons.lang3.ObjectUtils; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.prebid.server.analytics.AnalyticsReporter; | ||
import org.prebid.server.analytics.model.HttpContext; | ||
import org.prebid.server.analytics.model.NotificationEvent; | ||
import org.prebid.server.exception.InvalidRequestException; | ||
import org.prebid.server.proto.request.EventNotificationRequest; | ||
import org.prebid.server.util.ResourceUtil; | ||
|
||
import java.io.IOException; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
|
||
/** | ||
* Accepts notifications from browsers and mobile application for further processing by {@link AnalyticsReporter} | ||
* and responding with tracking pixel when requested. | ||
*/ | ||
public class NotificationEventHandler implements Handler<RoutingContext> { | ||
|
||
private static final String TRACKING_PIXEL_PNG = "static/tracking-pixel.png"; | ||
private static final String TRACKING_PIXEL_JPG = "static/tracking-pixel.jpg"; | ||
private static final String VIEW_TYPE = "view"; | ||
private static final String WIN_TYPE = "win"; | ||
private static final String FORMAT_PARAMETER = "format"; | ||
private static final String JPG = "jpg"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's have these renamed to |
||
private static final String PNG = "png"; | ||
private static final String JPG_HEADER = "image/jpeg"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are rather |
||
private static final String PNG_HEADER = "image/png"; | ||
|
||
private final AnalyticsReporter analyticsReporter; | ||
private final byte[] trackingPixelPng; | ||
private final byte[] trackingPixelJpg; | ||
|
||
private NotificationEventHandler(AnalyticsReporter analyticsReporter, byte[] trackingPixelPng, | ||
byte[] trackingPixelJpg) { | ||
this.analyticsReporter = analyticsReporter; | ||
this.trackingPixelJpg = trackingPixelJpg; | ||
this.trackingPixelPng = trackingPixelPng; | ||
} | ||
|
||
public static NotificationEventHandler create(AnalyticsReporter analyticsReporter) { | ||
return new NotificationEventHandler(Objects.requireNonNull(analyticsReporter), | ||
readTrackingPixel(TRACKING_PIXEL_PNG), readTrackingPixel(TRACKING_PIXEL_JPG)); | ||
} | ||
|
||
private static byte[] readTrackingPixel(String path) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move this method next to |
||
try { | ||
return ResourceUtil.readByteArrayFromClassPath(path); | ||
} catch (IOException e) { | ||
throw new IllegalArgumentException(String.format("Failed to load pixel image at %s", path), e); | ||
} | ||
} | ||
|
||
@Override | ||
public void handle(RoutingContext context) { | ||
final EventNotificationRequest eventNotificationRequest; | ||
try { | ||
eventNotificationRequest = makeEventRequest(context.getBody()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's correct - it is a Fixed tech spec to explicitly say it is a |
||
} catch (InvalidRequestException ex) { | ||
respondWithBadStatus(context, ex.getMessage()); | ||
return; | ||
} | ||
|
||
final NotificationEvent notificationEvent = new NotificationEvent(eventNotificationRequest.getType(), | ||
eventNotificationRequest.getBidId(), eventNotificationRequest.getBidder()); | ||
|
||
analyticsReporter.processEvent(notificationEvent); | ||
|
||
final Map<String, String> queryParams = HttpContext.from(context).getQueryParams(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is not need to create There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Has this been addressed somehow? |
||
final String format = queryParams.get(FORMAT_PARAMETER); | ||
|
||
try { | ||
validateEventRequestQueryParams(format); | ||
} catch (InvalidRequestException ex) { | ||
respondWithBadStatus(context, ex.getMessage()); | ||
return; | ||
} | ||
|
||
respondWithOkStatus(context, format); | ||
} | ||
|
||
private EventNotificationRequest makeEventRequest(Buffer body) { | ||
if (body == null) { | ||
throw new InvalidRequestException( | ||
"Request body was empty. Expected request with body has next fields: type, bidid and bidder."); | ||
} | ||
|
||
final EventNotificationRequest eventNotificationRequest; | ||
try { | ||
eventNotificationRequest = Json.decodeValue(body, EventNotificationRequest.class); | ||
} catch (DecodeException e) { | ||
throw new InvalidRequestException( | ||
"Request body couldn't be parsed. Expected request body has next fields: type, bidid and bidder."); | ||
} | ||
|
||
validateEventRequestBody(eventNotificationRequest); | ||
return eventNotificationRequest; | ||
} | ||
|
||
private void validateEventRequestBody(EventNotificationRequest eventNotificationRequest) { | ||
final String type = eventNotificationRequest.getType(); | ||
if (ObjectUtils.notEqual(type, VIEW_TYPE) && ObjectUtils.notEqual(type, WIN_TYPE)) { | ||
throw new InvalidRequestException(String.format( | ||
"Type is required parameter. Possible values are win and view, but was %s", type)); | ||
} | ||
|
||
final String bidId = eventNotificationRequest.getBidId(); | ||
if (StringUtils.isBlank(bidId)) { | ||
throw new InvalidRequestException("bidid is required and can't be empty."); | ||
} | ||
|
||
final String bidder = eventNotificationRequest.getBidder(); | ||
if (StringUtils.isBlank(bidder)) { | ||
throw new InvalidRequestException("bidder is required and can't be empty."); | ||
} | ||
|
||
} | ||
|
||
private void validateEventRequestQueryParams(String format) { | ||
if (format != null && ObjectUtils.notEqual(format, JPG) && ObjectUtils.notEqual(format, PNG)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Starting from here and onwards: this kind of conditions is not scalable enough and may bite you when third or fourth format will be added, especially this one and alike:
It will be easy to use it then for validation and response construction and it will be much safer when it will come to adding another format/ |
||
throw new InvalidRequestException("format when defined should has value of png or jpg."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to pull format parameter values from |
||
} | ||
} | ||
|
||
private void respondWithBadStatus(RoutingContext context, String message) { | ||
context.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) | ||
.end(String.format("%s%s", "Request is invalid: ", message)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
private void respondWithOkStatus(RoutingContext context, String format) { | ||
if (format != null) { | ||
context.response() | ||
.putHeader(HttpHeaders.CONTENT_TYPE, format.equals(JPG) ? JPG_HEADER : PNG_HEADER) | ||
.end(format.equals(JPG) ? Buffer.buffer(trackingPixelJpg) : Buffer.buffer(trackingPixelPng)); | ||
} else { | ||
context.response().end(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package org.prebid.server.proto.request; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Builder; | ||
import lombok.Value; | ||
|
||
@Builder | ||
@Value | ||
public class EventNotificationRequest { | ||
|
||
String type; | ||
|
||
@JsonProperty("bidid") | ||
String bidId; | ||
|
||
String bidder; | ||
|
||
String format; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ | |
import org.prebid.server.handler.CurrencyRatesHandler; | ||
import org.prebid.server.handler.ExceptionHandler; | ||
import org.prebid.server.handler.NoCacheHandler; | ||
import org.prebid.server.handler.NotificationEventHandler; | ||
import org.prebid.server.handler.OptoutHandler; | ||
import org.prebid.server.handler.SettingsCacheNotificationHandler; | ||
import org.prebid.server.handler.SetuidHandler; | ||
|
@@ -146,6 +147,7 @@ Router router(CookieHandler cookieHandler, | |
BidderParamHandler bidderParamHandler, | ||
BiddersHandler biddersHandler, | ||
BidderDetailsHandler bidderDetailsHandler, | ||
NotificationEventHandler notificationEventHandler, | ||
StaticHandler staticHandler) { | ||
|
||
final Router router = Router.router(vertx); | ||
|
@@ -164,6 +166,7 @@ Router router(CookieHandler cookieHandler, | |
router.get("/bidders/params").handler(bidderParamHandler); | ||
router.get("/info/bidders").handler(biddersHandler); | ||
router.get("/info/bidders/:bidderName").handler(bidderDetailsHandler); | ||
router.post("/event").handler(notificationEventHandler); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, it is implicit but it should be |
||
router.get("/static/*").handler(staticHandler); | ||
router.get("/").handler(staticHandler); // serves index.html by default | ||
|
||
|
@@ -307,6 +310,11 @@ BidderDetailsHandler bidderDetailsHandler(BidderCatalog bidderCatalog) { | |
return new BidderDetailsHandler(bidderCatalog); | ||
} | ||
|
||
@Bean | ||
NotificationEventHandler eventNotificationHandler(CompositeAnalyticsReporter compositeAnalyticsReporter) { | ||
return NotificationEventHandler.create(compositeAnalyticsReporter); | ||
} | ||
|
||
@Bean | ||
StaticHandler staticHandler() { | ||
return StaticHandler.create("static").setCachingEnabled(false); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use static factory
of
method for consistency