Add clientId metrics

Parse the `User-Agent` http request header to determine if the client is
known. If that is the case, increment the relevant `client_id` counter.

A `ProjectRequest` had now a generic `parameters` array with all the
request headers by default. Since we don't want to accidentally map any
of those from a form input, `BasicProjectRequest` contains only "public"
fields and is the type exposed at the controller level.

Closes gh-193
This commit is contained in:
Stephane Nicoll
2016-02-04 11:00:05 +01:00
parent dc570704c4
commit ac97fda208
8 changed files with 321 additions and 38 deletions

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.generator
/**
* The base settings of a project request. Only these can be bound by user's
* input.
*
* @author Stephane Nicoll
* @since 1.0
*/
class BasicProjectRequest {
List<String> style = []
List<String> dependencies = []
String name
String type
String description
String groupId
String artifactId
String version
String bootVersion
String packaging
String applicationName
String language
String packageName
String javaVersion
// The base directory to create in the archive - no baseDir by default
String baseDir
}

View File

@@ -16,6 +16,8 @@
package io.spring.initializr.generator
import io.spring.initializr.util.UserAgentWrapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.metrics.CounterService
import org.springframework.context.event.EventListener
@@ -56,6 +58,7 @@ class ProjectGenerationMetricsListener {
handlePackaging(request)
handleLanguage(request)
handleBootVersion(request)
handleUserAgent(request)
}
protected void handleDependencies(ProjectRequest request) {
@@ -102,6 +105,17 @@ class ProjectGenerationMetricsListener {
}
}
protected void handleUserAgent(ProjectRequest request) {
String userAgent = request.parameters['user-agent']
if (userAgent) {
UserAgentWrapper wrapper = new UserAgentWrapper(userAgent)
def information = wrapper.extractAgentInformation()
if (information) {
increment(key("client_id.$information.id.id"))
}
}
}
protected void increment(String key) {
counterService.increment(key)
}

View File

@@ -33,30 +33,17 @@ import io.spring.initializr.util.VersionRange
* @since 1.0
*/
@Slf4j
class ProjectRequest {
class ProjectRequest extends BasicProjectRequest {
/**
* The id of the starter to use if no dependency is defined.
*/
static final DEFAULT_STARTER = 'root_starter'
List<String> style = []
List<String> dependencies = []
String name
String type
String description
String groupId
String artifactId
String version
String bootVersion
String packaging
String applicationName
String language
String packageName
String javaVersion
// The base directory to create in the archive - no baseDir by default
String baseDir
/**
* Additional parameters that can be used to further identify the request.
*/
final Map<String,Object> parameters = [:]
// Resolved dependencies based on the ids provided by either "style" or "dependencies"
List<Dependency> resolvedDependencies

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.util
import org.springframework.util.Assert
/**
* Wraps a user agent header.
*
* @author Stephane Nicoll
* @since 1.0
*/
class UserAgentWrapper {
private static final TOOL_REGEX = '(.*)\\/(.*)'
private static final STS_REGEX = 'STS (.*)'
private final String userAgent
UserAgentWrapper(String userAgent) {
Assert.notNull(userAgent, "UserAgent must not be null")
this.userAgent = userAgent.trim()
}
boolean isSpringBootCli() {
return userAgent.startsWith(AgentId.SPRING_BOOT_CLI.name)
}
boolean isCurl() {
return userAgent.startsWith(AgentId.CURL.name)
}
boolean isHttpie() {
return userAgent.startsWith(AgentId.HTTPIE.name)
}
/**
* Detect the identifier of the agent and its version. Return {@code null} if the
* user agent managed by this instance is not recognized.
*/
AgentInformation extractAgentInformation() {
def matcher = (userAgent =~ TOOL_REGEX)
if (matcher.matches()) {
String name = matcher.group(1)
for (AgentId id : AgentId.values()) {
if (name.equals(id.name)) {
String version = matcher.group(2)
return new AgentInformation(id, version)
}
}
}
matcher = userAgent =~ STS_REGEX
if (matcher.matches()) {
return new AgentInformation(AgentId.STS, matcher.group(1))
}
if (userAgent.equals(AgentId.INTELLIJ_IDEA.name)) {
return new AgentInformation(AgentId.INTELLIJ_IDEA, null)
}
if (userAgent.contains('Mozilla/5.0')) { // Super heuristics
return new AgentInformation(AgentId.BROWSER, null)
}
return null
}
static class AgentInformation {
final AgentId id
final String version
AgentInformation(AgentId id, String version) {
this.id = id
this.version = version
}
}
static enum AgentId {
CURL('curl', 'curl'),
HTTPIE('httpie', 'HTTPie'),
SPRING_BOOT_CLI('spring', 'SpringBootCli'),
STS('sts', 'STS'),
INTELLIJ_IDEA('intellijidea', 'IntelliJ IDEA'),
BROWSER('browser', 'Browser')
final String id
final String name
AgentId(String id, String name) {
this.id = id
this.name = name
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import groovy.util.logging.Slf4j
import io.spring.initializr.generator.BasicProjectRequest
import io.spring.initializr.generator.CommandLineHelpGenerator
import io.spring.initializr.generator.ProjectGenerator
import io.spring.initializr.generator.ProjectRequest
@@ -30,6 +31,7 @@ import io.spring.initializr.mapper.InitializrMetadataV2JsonMapper
import io.spring.initializr.mapper.InitializrMetadataVersion
import io.spring.initializr.metadata.DependencyMetadataProvider
import io.spring.initializr.metadata.InitializrMetadata
import io.spring.initializr.util.UserAgentWrapper
import io.spring.initializr.util.Version
import org.springframework.beans.factory.annotation.Autowired
@@ -71,8 +73,9 @@ class MainController extends AbstractInitializrController {
@ModelAttribute
ProjectRequest projectRequest() {
BasicProjectRequest projectRequest(@RequestHeader Map<String,String> headers) {
def request = new ProjectRequest()
request.parameters << headers
request.initialize(metadataProvider.get())
request
}
@@ -97,15 +100,16 @@ class MainController extends AbstractInitializrController {
def builder = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN)
if (userAgent) {
if (userAgent.startsWith(WebConfig.CURL_USER_AGENT_PREFIX)) {
UserAgentWrapper wrapper = new UserAgentWrapper(userAgent)
if (wrapper.isCurl()) {
def content = commandLineHelpGenerator.generateCurlCapabilities(metadata, appUrl)
return builder.eTag(createUniqueId(content)).body(content)
}
if (userAgent.startsWith(WebConfig.HTTPIE_USER_AGENT_PREFIX)) {
if (wrapper.isHttpie()) {
def content = commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl)
return builder.eTag(createUniqueId(content)).body(content)
}
if (userAgent.startsWith(WebConfig.SPRING_BOOT_CLI_AGENT_PREFIX)) {
if (wrapper.isSpringBootCli()) {
def content = commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl)
return builder.eTag(createUniqueId(content)).body(content)
}
@@ -182,21 +186,22 @@ class MainController extends AbstractInitializrController {
@RequestMapping('/pom')
@ResponseBody
ResponseEntity<byte[]> pom(ProjectRequest request) {
def mavenPom = projectGenerator.generateMavenPom(request)
ResponseEntity<byte[]> pom(BasicProjectRequest request) {
def mavenPom = projectGenerator.generateMavenPom((ProjectRequest) request)
createResponseEntity(mavenPom, 'application/octet-stream', 'pom.xml')
}
@RequestMapping('/build')
@ResponseBody
ResponseEntity<byte[]> gradle(ProjectRequest request) {
def gradleBuild = projectGenerator.generateGradleBuild(request)
ResponseEntity<byte[]> gradle(BasicProjectRequest request) {
def gradleBuild = projectGenerator.generateGradleBuild((ProjectRequest) request)
createResponseEntity(gradleBuild, 'application/octet-stream', 'build.gradle')
}
@RequestMapping('/starter.zip')
@ResponseBody
ResponseEntity<byte[]> springZip(ProjectRequest request) {
ResponseEntity<byte[]> springZip(BasicProjectRequest basicRequest) {
ProjectRequest request = (ProjectRequest) basicRequest
def dir = projectGenerator.generateProjectStructure(request)
def download = projectGenerator.createDistributionFile(dir, '.zip')
@@ -212,7 +217,8 @@ class MainController extends AbstractInitializrController {
@RequestMapping(value = '/starter.tgz', produces = 'application/x-compress')
@ResponseBody
ResponseEntity<byte[]> springTgz(ProjectRequest request) {
ResponseEntity<byte[]> springTgz(BasicProjectRequest basicRequest) {
ProjectRequest request = (ProjectRequest) basicRequest
def dir = projectGenerator.generateProjectStructure(request)
def download = projectGenerator.createDistributionFile(dir, '.tgz')

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package io.spring.initializr.web
import io.spring.initializr.util.UserAgentWrapper
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.web.HttpMediaTypeNotAcceptableException
@@ -35,12 +37,6 @@ import javax.servlet.http.HttpServletRequest
*/
class WebConfig extends WebMvcConfigurerAdapter {
static final String CURL_USER_AGENT_PREFIX = 'curl'
static final String HTTPIE_USER_AGENT_PREFIX = 'HTTPie'
static final String SPRING_BOOT_CLI_AGENT_PREFIX = 'SpringBootCli'
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentTypeStrategy(new CommandLineContentNegotiationStrategy())
@@ -63,7 +59,8 @@ class WebConfig extends WebMvcConfigurerAdapter {
}
String userAgent = request.getHeader(HttpHeaders.USER_AGENT)
if (userAgent) {
if (userAgent.startsWith(CURL_USER_AGENT_PREFIX) || userAgent.startsWith(HTTPIE_USER_AGENT_PREFIX)) {
UserAgentWrapper wrapper = new UserAgentWrapper(userAgent)
if (wrapper.isCurl() || wrapper.isHttpie()) {
return Collections.singletonList(MediaType.TEXT_PLAIN)
}
}

View File

@@ -189,6 +189,15 @@ class ProjectGenerationMetricsListenerTests {
metricsAssert.hasValue(1, 'initializr.boot_version.1_0_2_RELEASE')
}
@Test
void userAgentAvailable() {
def request = initialize()
request.parameters['user-agent'] = 'HTTPie/0.9.2'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.client_id.httpie')
}
@Test
void collectAllMetrics() {
def request = initialize()
@@ -198,6 +207,7 @@ class ProjectGenerationMetricsListenerTests {
request.javaVersion = '1.6'
request.language = 'groovy'
request.bootVersion = '1.0.2.RELEASE'
request.parameters['user-agent'] = 'SpringBootCli/1.3.0.RELEASE'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
@@ -205,7 +215,8 @@ class ProjectGenerationMetricsListenerTests {
'initializr.dependency.web', 'initializr.dependency.security',
'initializr.type.gradle-project', 'initializr.packaging.jar',
'initializr.java_version.1_6', 'initializr.language.groovy',
'initializr.boot_version.1_0_2_RELEASE').metricsCount(8)
'initializr.boot_version.1_0_2_RELEASE',
'initializr.client_id.spring').metricsCount(9)
}
@Test

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.util
import org.junit.Test
import static org.hamcrest.CoreMatchers.equalTo
import static org.hamcrest.CoreMatchers.is
import static org.hamcrest.CoreMatchers.nullValue
import static org.junit.Assert.assertThat
/**
* @author Stephane Nicoll
*/
class UserAgentWrapperTests {
@Test
void checkCurl() {
UserAgentWrapper userAgent = new UserAgentWrapper('curl/1.2.4')
assertThat(userAgent.isCurl(), is(true))
assertThat(userAgent.isHttpie(), is(false))
assertThat(userAgent.isSpringBootCli(), is(false))
def information = userAgent.extractAgentInformation()
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.CURL))
assertThat(information.version, equalTo('1.2.4'))
}
@Test
void checkHttpie() {
UserAgentWrapper userAgent = new UserAgentWrapper('HTTPie/0.8.0')
assertThat(userAgent.isCurl(), is(false))
assertThat(userAgent.isHttpie(), is(true))
assertThat(userAgent.isSpringBootCli(), is(false))
def information = userAgent.extractAgentInformation()
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.HTTPIE))
assertThat(information.version, equalTo('0.8.0'))
}
@Test
void checkSpringBootCli() {
UserAgentWrapper userAgent = new UserAgentWrapper('SpringBootCli/1.3.1.RELEASE')
assertThat(userAgent.isCurl(), is(false))
assertThat(userAgent.isHttpie(), is(false))
assertThat(userAgent.isSpringBootCli(), is(true))
def information = userAgent.extractAgentInformation()
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.SPRING_BOOT_CLI))
assertThat(information.version, equalTo('1.3.1.RELEASE'))
}
@Test
void checkSts() {
UserAgentWrapper userAgent = new UserAgentWrapper('STS 3.7.0.201506251244-RELEASE')
assertThat(userAgent.isCurl(), is(false))
assertThat(userAgent.isHttpie(), is(false))
assertThat(userAgent.isSpringBootCli(), is(false))
def information = userAgent.extractAgentInformation()
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.STS))
assertThat(information.version, equalTo('3.7.0.201506251244-RELEASE'))
}
@Test
void checkIntelliJIDEA() {
UserAgentWrapper userAgent = new UserAgentWrapper('IntelliJ IDEA')
assertThat(userAgent.isCurl(), is(false))
assertThat(userAgent.isHttpie(), is(false))
assertThat(userAgent.isSpringBootCli(), is(false))
def information = userAgent.extractAgentInformation()
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.INTELLIJ_IDEA))
assertThat(information.version, is(nullValue()))
}
@Test
void checkGenericBrowser() {
UserAgentWrapper userAgent =
new UserAgentWrapper('Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MMB29K) ')
assertThat(userAgent.isCurl(), is(false))
assertThat(userAgent.isHttpie(), is(false))
assertThat(userAgent.isSpringBootCli(), is(false))
def information = userAgent.extractAgentInformation()
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.BROWSER))
assertThat(information.version, is(nullValue()))
}
@Test
void checkRobot() {
UserAgentWrapper userAgent =
new UserAgentWrapper('Googlebot-Mobile')
assertThat(userAgent.isCurl(), is(false))
assertThat(userAgent.isHttpie(), is(false))
assertThat(userAgent.isSpringBootCli(), is(false))
def information = userAgent.extractAgentInformation()
assertThat(information, is(nullValue()))
}
}