Skip to content

Commit

Permalink
[hab] Add lightweight analytics to hab CLI which is defaulted to op…
Browse files Browse the repository at this point in the history
…t-out.

The `hab` command-line tool will optionally send anonymous usage data to
Habitat's Google Analytics account. This is a strictly opt-in activity
and no tracking will occur unless you respond affirmatively to the
question during `hab setup`. If you do not use `hab setup`, no data will
ever be sent.

We collect this data to help improve Habitat's user experience: for
example, to know what tasks users are performing, and which ones they
are having trouble with (e.g. mistyping command line arguments).

By _anonymous_ we mean that all identifying information about you is
removed before we send the data. This includes the removal of any
information about what packages you are building, or what origins you
are using. For example, if you were building the package
`yourname/yourapp`, and you typed `hab pkg build -k yourkey
yourname/yourapp`, the fact that you were performing the `pkg build`
operation would be transmitted. Neither the name of the specific package
you are building, nor the fact that you are using the `yourkey` key to
sign that package would be transmitted.

Please do not hesitate to [contact us](mailto:[email protected]) if you
have questions or concerns about the use of Google Analytics within the
Habitat product.

Note that this module is highly documented, even inside functions with
the intent of guiding a user through the implementation who may not
necessarily be familiar with Rust code. Given the
"must-not-impact-the-user" nature of this code, it tends to be much more
explicit than regular, idomatic Rust code. But at least you can see a
lot of `match` expressions in action :)

Subcommand Invocations
======================

The following is a complete list of all pre-selected commands which are
reported:

* `apply`
* `artifact upload`
* `config apply`
* `file upload`
* `origin key generate`
* `pkg build`
* `ring key generate`
* `service key generate`
* `studio build`
* `studio enter`
* `user key generate`

Subcommands Which Attempt Submission of Events
==============================================

The only time events will be sent to the Google Analytics API is when
certain subcommands are invoked which require network access. These
subcommands are:

* `apply`
* `artifact upload`
* `config apply`
* `file upload`
* `install`
* `origin key upload`
* `pkg install`

For all other subcommands, even those which report events, the event
payload is saved to a cached file under the analytics cache directory
(`/hab/cache/analytics` for a root user and `$HOME/.hab/cache/analytics`
for a non-root user).

Event Data Breakdown
====================

For each event that is reported, a set of information is bundled into
the payload. Here is a breakdown of each key/value entry:

`v=1`
-----

The [Protocol
Version](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#v)
which is currently only ever the integer value of `1`.

`tid=UA-XXXXXXX-X`
------------------

The [Track
ID](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#tid)
which represents this product and is currently hard coded as
`"UA-6369228-7"`.

`cid=f673faaf-6ba1-4e60-b819-e2d51e4ad6f1`
------------------------------------------

The [Client
ID](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid)
which is a randomly generated [UUID
v4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29)
and written into the system or user's analytics cache
(`/hab/cache/analytics/CLIENT_ID` when the program is invoked as the
root user and `$HOME/.hab/analytics/CLIENT_ID` when invoked by a
non-root user). This is not intended to track individual users or
systems, but rather show patterns of usage in aggregate. For example:
"In general, users who generally start with `hab studio enter` tend to
migrate to using `hab pkg build` over time".

`t=event`
---------

The [Hit
Type](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#t).
This value is hard coded as `"event"` as it is a required Google
Analtyics field for all Hit Events.

`aip=1`
------

Enables [Anonymize
IP](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#aip).
This entry ensures that the sender's IP address will not be captured and
will be anonymized.  The value is hard coded as the integer `1`.

`an=hab`
-------

The [Application
Name](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#an)
of the program sending the event. For this program the value is
currently hard coded as `"hab"`.

`av=0.6.0%2F20160604180457`
---------------------------

The [Application
Version](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#av)
of the program sending the event. This version string will be the same
value as reported when asking for the program's version on the command
line. Note that this field may contain characters that must be percent
encoded.

`ds=cli--hab`
-------------

The [Data
Source])https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ds
which represents the program which generated the event data. For this
program the value is currently hardcoded as `"cli--hab"`.

`ec=invoke`
-----------

The [Event
Category](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec)
which corresponds to the type of event being sent. Currently there are
only 2 possible values: `"invoke"` for subcommand invocations and
`"clierror"` for CLI errors.

`ea=hab--pkg--build`
--------------------

The [Event
Action](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea)
which breaks down differently depending on the type of event. For
subcommand invocations (where `"ec=invoke"`), the value is the
subcommand invoked with no further arguments, options, or flags. Any
spaces are replaced with a doubledash, as in: `"hab--studio--enter"` or
`"hab--artifact--upload"`. For CLI errors (where `"ec=clierror"`), the
value is the type of CLI error followed by a double dash and terminated
with the subcommand which was invoked (also containing no further
arguments, options, or flags). As before any spaces in the subcommand
are replaced with a double dash, as in:
`"InvalidSubcommand--hab-whoops"`.

User-Agent HTTP Header
======================

A user agent string is also included in the HTTP/POST to the Google
Analytics API it is of the form:

```text
<PRODUCT>/<VERSION> (<TARGET>; <KERNEL_RELEASE>)
```

where:

* `<PRODUCT>`: is the provided product name. For this program the value
* is currently hard coded
  as `"hab"`.
* `<VERSION>`: is the provided version string which may also inclde a
* release number. This is
  the same version obtained when running the help or version
subcommands.
* `<TARGET>`: is the machine architecture and the kernel seperated by a
* dash in lower case.
* `<KERNEL_RELEASE>`: is the kernel release string from `uname`.

For example:

```text
hab/0.6.0/20160606153031 (x86_64-darwin; 14.5.0)
```

Signed-off-by: Fletcher Nichol <[email protected]>
  • Loading branch information
fnichol committed Jun 6, 2016
1 parent 4268bb2 commit a7794aa
Show file tree
Hide file tree
Showing 26 changed files with 904 additions and 8 deletions.
12 changes: 12 additions & 0 deletions components/builder-api/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/builder-dbcache/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/builder-jobsrv/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/builder-protocol/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/builder-sessionsrv/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/builder-vault/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/builder-worker/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/common/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions components/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ version = "0.6.0"
authors = ["Adam Jacob <[email protected]>", "Jamie Winsor <[email protected]>", "Fletcher Nichol <[email protected]>", "Joshua Timberman <[email protected]>", "Dave Parfitt <[email protected]>"]

[dependencies]
errno = "*"
lazy_static = "*"
libarchive = "*"
libc = "*"
log = "*"
regex = "*"
rustc-serialize = "*"
Expand Down
4 changes: 4 additions & 0 deletions components/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ pub enum Error {
RegexParse(regex::Error),
/// When an error occurs converting a `String` from a UTF-8 byte vector.
StringFromUtf8Error(string::FromUtf8Error),
/// Occurs when a `uname` libc call returns an error.
UnameFailed(String),
/// When an error occurs attempting to interpret a sequence of u8 as a string.
Utf8Error(str::Utf8Error),
/// When an error occurs attempting to parse a string into a URL.
Expand Down Expand Up @@ -135,6 +137,7 @@ impl fmt::Display for Error {
Error::PermissionFailed => format!("Failed to set permissions"),
Error::RegexParse(ref e) => format!("{}", e),
Error::StringFromUtf8Error(ref e) => format!("{}", e),
Error::UnameFailed(ref e) => format!("{}", e),
Error::Utf8Error(ref e) => format!("{}", e),
Error::UrlParseError(ref e) => format!("{}", e),
};
Expand Down Expand Up @@ -180,6 +183,7 @@ impl error::Error for Error {
Error::PermissionFailed => "Failed to set permissions",
Error::RegexParse(_) => "Failed to parse a regular expression",
Error::StringFromUtf8Error(_) => "Failed to convert a string from a Vec<u8> as UTF-8",
Error::UnameFailed(_) => "uname failed",
Error::Utf8Error(_) => "Failed to interpret a sequence of bytes as a string",
Error::UrlParseError(ref err) => err.description(),
}
Expand Down
21 changes: 21 additions & 0 deletions components/core/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use env as henv;
pub const FS_ROOT_PATH: &'static str = "/";
/// The default root path of the Habitat filesytem
pub const ROOT_PATH: &'static str = "hab";
/// The default path for any analytics related files
pub const CACHE_ANALYTICS_PATH: &'static str = "hab/cache/analytics";
/// The default download root path for package artifacts, used on package installation
pub const CACHE_ARTIFACT_PATH: &'static str = "hab/cache/artifacts";
/// The default path where cryptographic keys are stored
Expand All @@ -32,6 +34,17 @@ const SVC_PATH: &'static str = "hab/svc";
lazy_static! {
static ref EUID: u32 = users::get_effective_uid();

static ref MY_CACHE_ANALYTICS_PATH: PathBuf = {
if *EUID == 0u32 {
PathBuf::from(CACHE_ANALYTICS_PATH)
} else {
match env::home_dir() {
Some(home) => home.join(format!(".{}", CACHE_ANALYTICS_PATH)),
None => PathBuf::from(CACHE_ANALYTICS_PATH),
}
}
};

static ref MY_CACHE_ARTIFACT_PATH: PathBuf = {
if *EUID == 0u32 {
PathBuf::from(CACHE_ARTIFACT_PATH)
Expand Down Expand Up @@ -77,6 +90,14 @@ lazy_static! {
};
}

/// Returns the path to the analytics cache, optionally taking a custom filesystem root.
pub fn cache_analytics_path(fs_root_path: Option<&Path>) -> PathBuf {
match fs_root_path {
Some(fs_root_path) => Path::new(fs_root_path).join(&*MY_CACHE_ANALYTICS_PATH),
None => Path::new(FS_ROOT_PATH).join(&*MY_CACHE_ANALYTICS_PATH),
}
}

/// Returns the path to the artifacts cache, optionally taking a custom filesystem root.
pub fn cache_artifact_path(fs_root_path: Option<&Path>) -> PathBuf {
match fs_root_path {
Expand Down
2 changes: 2 additions & 0 deletions components/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
// the Software until such time that the Software is made available under an
// open source license such as the Apache 2.0 License.

extern crate errno;
#[cfg(test)]
extern crate hyper;
#[macro_use]
extern crate lazy_static;
extern crate libc;
extern crate libarchive;
#[macro_use]
extern crate log;
Expand Down
38 changes: 37 additions & 1 deletion components/core/src/util/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
// the Software until such time that the Software is made available under an
// open source license such as the Apache 2.0 License.

use error::{Error, Result};
use std::ffi::CStr;
use std::mem;
use std::process::Command;

use errno::errno;
use libc;

use error::{Error, Result};

pub fn ip(path: Option<&str>) -> Result<String> {
debug!("Shelling out to determine IP address");
let mut cmd = Command::new("sh");
Expand All @@ -31,3 +37,33 @@ pub fn ip(path: Option<&str>) -> Result<String> {
}
}
}

#[derive(Debug)]
pub struct Uname {
pub sys_name: String,
pub node_name: String,
pub release: String,
pub version: String,
pub machine: String,
}

pub fn uname() -> Result<Uname> {
unsafe { uname_libc() }
}

unsafe fn uname_libc() -> Result<Uname> {
let mut utsname: libc::utsname = mem::uninitialized();
let rv = libc::uname(&mut utsname);
if rv < 0 {
let errno = errno();
let code = errno.0 as i32;
return Err(Error::UnameFailed(format!("Error {} when calling uname: {}", code, errno)));
}
Ok(Uname {
sys_name: CStr::from_ptr(utsname.sysname.as_ptr()).to_string_lossy().into_owned(),
node_name: CStr::from_ptr(utsname.nodename.as_ptr()).to_string_lossy().into_owned(),
release: CStr::from_ptr(utsname.release.as_ptr()).to_string_lossy().into_owned(),
version: CStr::from_ptr(utsname.version.as_ptr()).to_string_lossy().into_owned(),
machine: CStr::from_ptr(utsname.machine.as_ptr()).to_string_lossy().into_owned(),
})
}
Loading

0 comments on commit a7794aa

Please sign in to comment.