Skip to content
This repository was archived by the owner on Apr 22, 2022. It is now read-only.

Commit 666f362

Browse files
committed
Implement support for 204 No Content event responses.
This is configurable, and intended for the web source instead of the 1x1 GIF.
1 parent b51e151 commit 666f362

9 files changed

+121
-24
lines changed

Diff for: src/main/java/io/divolte/server/BrowserSource.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public BrowserSource(final ValidatedConfiguration vc,
4949
this(sourceName,
5050
vc.configuration().getSourceConfiguration(sourceName, BrowserSourceConfiguration.class).prefix,
5151
vc.configuration().getSourceConfiguration(sourceName, BrowserSourceConfiguration.class).eventSuffix,
52+
vc.configuration().getSourceConfiguration(sourceName, BrowserSourceConfiguration.class).useNoContent,
5253
loadTrackingJavaScript(vc, sourceName),
5354
processingPool,
5455
vc.configuration().sourceIndex(sourceName));
@@ -57,6 +58,7 @@ public BrowserSource(final ValidatedConfiguration vc,
5758
private BrowserSource(final String sourceName,
5859
final String pathPrefix,
5960
final String eventSuffix,
61+
final boolean useNoContent,
6062
final TrackingJavaScriptResource trackingJavascript,
6163
final IncomingRequestProcessingPool processingPool,
6264
final int sourceIndex) {
@@ -65,7 +67,7 @@ private BrowserSource(final String sourceName,
6567
this.eventSuffix = eventSuffix;
6668
javascriptName = trackingJavascript.getScriptName();
6769
javascriptHandler = new AllowedMethodsHandler(new JavaScriptHandler(trackingJavascript), Methods.GET);
68-
final ClientSideCookieEventHandler clientSideCookieEventHandler = new ClientSideCookieEventHandler(processingPool, sourceIndex);
70+
final ClientSideCookieEventHandler clientSideCookieEventHandler = new ClientSideCookieEventHandler(processingPool, useNoContent, sourceIndex);
6971
eventHandler = new AllowedMethodsHandler(clientSideCookieEventHandler, Methods.GET);
7072
}
7173

Diff for: src/main/java/io/divolte/server/ClientSideCookieEventHandler.java

+30-21
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public final class ClientSideCookieEventHandler implements HttpHandler {
5656
private final static ETag SENTINEL_ETAG = new ETag(false, "6b3edc43-20ec-4078-bc47-e965dd76b88a");
5757
private final static String SENTINEL_ETAG_VALUE = SENTINEL_ETAG.toString();
5858

59-
private final ByteBuffer transparentImage;
59+
private final Optional<ByteBuffer> transparentImage;
6060
private final IncomingRequestProcessingPool processingPool;
6161
private final int sourceIndex;
6262

@@ -82,17 +82,23 @@ public final class ClientSideCookieEventHandler implements HttpHandler {
8282

8383
private static final ObjectReader EVENT_PARAMETERS_READER = new ObjectMapper(new MincodeFactory()).reader();
8484

85-
public ClientSideCookieEventHandler(final IncomingRequestProcessingPool processingPool, final int sourceIndex) {
85+
public ClientSideCookieEventHandler(final IncomingRequestProcessingPool processingPool,
86+
final boolean sendEmptyResponse,
87+
final int sourceIndex) {
8688
this.sourceIndex = sourceIndex;
8789
this.processingPool = Objects.requireNonNull(processingPool);
8890

89-
try {
90-
this.transparentImage = ByteBuffer.wrap(
91-
Resources.toByteArray(Resources.getResource("transparent1x1.gif"))
92-
).asReadOnlyBuffer();
93-
} catch (final IOException e) {
94-
// Should throw something more specific than this.
95-
throw new RuntimeException("Could not load transparent image resource.", e);
91+
if (sendEmptyResponse) {
92+
this.transparentImage = Optional.empty();
93+
} else {
94+
try {
95+
this.transparentImage = Optional.of(ByteBuffer.wrap(
96+
Resources.toByteArray(Resources.getResource("transparent1x1.gif"))
97+
).asReadOnlyBuffer());
98+
} catch (final IOException e) {
99+
// Should throw something more specific than this.
100+
throw new RuntimeException("Could not load transparent image resource.", e);
101+
}
96102
}
97103
}
98104

@@ -109,17 +115,21 @@ public void handleRequest(final HttpServerExchange exchange) {
109115
* As a last resort, we try to detect duplicates via the ETag header.
110116
*/
111117
exchange.getResponseHeaders()
112-
.put(Headers.CONTENT_TYPE, "image/gif")
113118
.put(Headers.ETAG, SENTINEL_ETAG_VALUE)
114119
.put(Headers.CACHE_CONTROL, "private, no-cache, proxy-revalidate")
115120
.put(Headers.PRAGMA, "no-cache")
116121
.put(Headers.EXPIRES, "Fri, 14 Apr 1995 11:30:00 GMT");
117122

118123
// If an ETag is present, this is a duplicate event.
119124
if (ETagUtils.handleIfNoneMatch(exchange, SENTINEL_ETAG, true)) {
120-
// Default status code what we want: 200 OK.
121125
// Sending the response before logging the event!
122-
exchange.getResponseSender().send(transparentImage.slice());
126+
// We send either 200 OK or 204 No Content, depending on whether we have the image to send.
127+
if (transparentImage.isPresent()) {
128+
exchange.getResponseSender().send(transparentImage.get().slice());
129+
} else {
130+
exchange.setStatusCode(StatusCodes.NO_CONTENT)
131+
.endExchange();
132+
}
123133

124134
try {
125135
logEvent(exchange);
@@ -131,8 +141,8 @@ public void handleRequest(final HttpServerExchange exchange) {
131141
if (logger.isDebugEnabled()) {
132142
logger.debug("Ignoring duplicate event from {}: {}", sourceAddress, getFullUrl(exchange));
133143
}
134-
exchange.setStatusCode(StatusCodes.NOT_MODIFIED);
135-
exchange.endExchange();
144+
exchange.setStatusCode(StatusCodes.NOT_MODIFIED)
145+
.endExchange();
136146
}
137147
}
138148

@@ -166,13 +176,12 @@ public DivolteEvent parseRequest() throws IncompleteRequestException {
166176
final boolean isFirstInSession = queryParamFromExchange(exchange, FIRST_IN_SESSION_QUERY_PARAM).map(TRUE_STRING::equals).orElseThrow(IncompleteRequestException::new);
167177
final Instant clientTimeStamp = Instant.ofEpochMilli(queryParamFromExchange(exchange, CLIENT_TIMESTAMP_QUERY_PARAM).map(ClientSideCookieEventHandler::tryParseBase36Long).orElseThrow(IncompleteRequestException::new));
168178

169-
final DivolteEvent event = DivolteEvent.createBrowserEvent(exchange, corrupt, partyId, sessionId, eventId,
170-
requestTime, clientTimeStamp,
171-
isNewPartyId, isFirstInSession,
172-
queryParamFromExchange(exchange, EVENT_TYPE_QUERY_PARAM),
173-
eventParameterSupplier(exchange),
174-
browserEventData(exchange, pageViewId));
175-
return event;
179+
return DivolteEvent.createBrowserEvent(exchange, corrupt, partyId, sessionId, eventId,
180+
requestTime, clientTimeStamp, isNewPartyId,
181+
isFirstInSession,
182+
queryParamFromExchange(exchange, EVENT_TYPE_QUERY_PARAM),
183+
eventParameterSupplier(exchange),
184+
browserEventData(exchange, pageViewId));
176185
}
177186
}
178187

Diff for: src/main/java/io/divolte/server/config/BrowserSourceConfiguration.java

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class BrowserSourceConfiguration extends SourceConfiguration {
2222
private static final String DEFAULT_PARTY_TIMEOUT = "730 days";
2323
private static final String DEFAULT_SESSION_COOKIE = "_dvs";
2424
private static final String DEFAULT_SESSION_TIMEOUT = "30 minutes";
25+
private static final String DEFAULT_USE_NO_CONTENT = "false";
2526
private static final String DEFAULT_PREFIX = "/";
2627
private static final String DEFAULT_EVENT_SUFFIX = "csc-event";
2728

@@ -33,6 +34,7 @@ public class BrowserSourceConfiguration extends SourceConfiguration {
3334
DurationDeserializer.parseDuration(DEFAULT_PARTY_TIMEOUT),
3435
DEFAULT_SESSION_COOKIE,
3536
DurationDeserializer.parseDuration(DEFAULT_SESSION_TIMEOUT),
37+
DEFAULT_USE_NO_CONTENT,
3638
JavascriptConfiguration.DEFAULT_JAVASCRIPT_CONFIGURATION);
3739

3840
public final String prefix;
@@ -42,6 +44,7 @@ public class BrowserSourceConfiguration extends SourceConfiguration {
4244
public final Duration partyTimeout;
4345
public final String sessionCookie;
4446
public final Duration sessionTimeout;
47+
public final boolean useNoContent;
4548

4649
@Valid
4750
public final JavascriptConfiguration javascript;
@@ -55,6 +58,7 @@ public class BrowserSourceConfiguration extends SourceConfiguration {
5558
@JsonProperty(defaultValue=DEFAULT_PARTY_TIMEOUT) final Duration partyTimeout,
5659
@JsonProperty(defaultValue=DEFAULT_SESSION_COOKIE) final String sessionCookie,
5760
@JsonProperty(defaultValue=DEFAULT_SESSION_TIMEOUT) final Duration sessionTimeout,
61+
@JsonProperty(defaultValue=DEFAULT_USE_NO_CONTENT) final String useNoContent,
5862
final JavascriptConfiguration javascript) {
5963
// TODO: register a custom deserializer with Jackson that uses the defaultValue property from the annotation to fix this
6064
this.prefix = Optional.ofNullable(prefix).map(BrowserSourceConfiguration::ensureTrailingSlash).orElse(DEFAULT_PREFIX);
@@ -64,6 +68,7 @@ public class BrowserSourceConfiguration extends SourceConfiguration {
6468
this.partyTimeout = Optional.ofNullable(partyTimeout).orElseGet(() -> DurationDeserializer.parseDuration(DEFAULT_PARTY_TIMEOUT));
6569
this.sessionCookie = Optional.ofNullable(sessionCookie).orElse(DEFAULT_SESSION_COOKIE);
6670
this.sessionTimeout = Optional.ofNullable(sessionTimeout).orElseGet(() -> DurationDeserializer.parseDuration(DEFAULT_SESSION_TIMEOUT));
71+
this.useNoContent = Boolean.valueOf(Optional.ofNullable(useNoContent).orElse(DEFAULT_USE_NO_CONTENT));
6772
this.javascript = Optional.ofNullable(javascript).orElse(JavascriptConfiguration.DEFAULT_JAVASCRIPT_CONFIGURATION);
6873
}
6974

@@ -81,6 +86,7 @@ protected MoreObjects.ToStringHelper toStringHelper() {
8186
.add("partyTimeout", partyTimeout)
8287
.add("sessionCookie", sessionCookie)
8388
.add("sessionTimeout", sessionTimeout)
89+
.add("useNoContent", useNoContent)
8490
.add("javascript", javascript);
8591
}
8692

Diff for: src/main/java/io/divolte/server/js/TrackingJavaScriptResource.java

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ private static ImmutableMap<String, Object> createScriptConstants(final BrowserS
5353
builder.put(SCRIPT_CONSTANT_NAME, browserSourceConfiguration.javascript.name);
5454
builder.put("EVENT_SUFFIX", browserSourceConfiguration.eventSuffix);
5555
builder.put("AUTO_PAGE_VIEW_EVENT", browserSourceConfiguration.javascript.autoPageViewEvent);
56+
builder.put("IMAGE_EXPECT_NOCONTENT", browserSourceConfiguration.useNoContent);
5657
return builder.build();
5758
}
5859

Diff for: src/main/resources/divolte.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ var SCRIPT_NAME = 'divolte.js';
3838
var EVENT_SUFFIX = 'csc-event';
3939
/** @define {boolean} */
4040
var AUTO_PAGE_VIEW_EVENT = true;
41+
/** @define {boolean} */
42+
var IMAGE_EXPECT_NOCONTENT = false;
4143

4244
(function (global, factory) {
4345
factory(global);
@@ -674,8 +676,10 @@ var AUTO_PAGE_VIEW_EVENT = true;
674676
signalQueue.onFirstPendingEventCompleted();
675677
};
676678
image.onload = completionHandler;
677-
image.onerror = !LOGGING ? completionHandler : function(event) {
678-
warn("Error delivering event", firstPendingEvent);
679+
image.onerror = IMAGE_EXPECT_NOCONTENT || !LOGGING ? completionHandler : function(event) {
680+
if (!IMAGE_EXPECT_NOCONTENT) {
681+
error("Error delivering event", firstPendingEvent);
682+
}
679683
completionHandler();
680684
};
681685
// TODO: Implement a timeout for when neither onload or onerror are invoked.

Diff for: src/test/java/io/divolte/server/SeleniumJavaScriptTest.java

+19
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,23 @@ public void shouldUseConfiguredEventSuffix() throws Exception {
283283

284284
assertEquals("pageView", eventData.eventType.get());
285285
}
286+
287+
@Test
288+
public void shouldSupportNoContentResponsesForEventDelivery() throws Exception {
289+
doSetUp("selenium-test-use-no-content-response.conf");
290+
Preconditions.checkState(null != driver && null != server);
291+
driver.get(urlOf(BASIC));
292+
293+
final EventPayload firstPayload = server.waitForEvent();
294+
final DivolteEvent firstEventData = firstPayload.event;
295+
assertEquals("pageView", firstEventData.eventType.get());
296+
297+
driver.findElement(By.id("custom")).click();
298+
final EventPayload secondPayload = server.waitForEvent();
299+
final DivolteEvent secondEventData = secondPayload.event;
300+
301+
assertTrue(secondEventData.eventType.isPresent());
302+
assertEquals("custom", secondEventData.eventType.get());
303+
// No need to check the parameters; we're only concerned that the event arrives.
304+
}
286305
}

Diff for: src/test/java/io/divolte/server/ServerSinkSourceConfigurationTest.java

+7
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ public void shouldSupportLongSourcePaths() throws IOException, InterruptedExcept
178178
testServer.get().waitForEvent();
179179
}
180180

181+
@Test
182+
public void shouldSupportNoContentResponsesFromBrowserSources() throws IOException {
183+
// Test that the browser source returns 204 No Content when configured so.
184+
startServer("browser-source-no-content-response.conf");
185+
request("", 204);
186+
}
187+
181188
@Test
182189
public void shouldSupportMultipleBrowserSources() throws IOException, InterruptedException {
183190
// Test that multiple browser sources are supported.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// Copyright 2016 GoDataDriven B.V.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
// Specify a single browser source that isn't used by any mappings.
18+
divolte {
19+
sources.no-content-source {
20+
type = browser
21+
use_no_content = true
22+
}
23+
mappings.test = {
24+
sources = [no-content-source]
25+
// Need at least one sink.
26+
sinks = [hdfs]
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Copyright 2016 GoDataDriven B.V.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
// Instead of using the default 'csc-event' endpoint, use a different value.
18+
divolte.sources.browser {
19+
type = browser
20+
use_no_content = true
21+
}

0 commit comments

Comments
 (0)