FEATURE: track statistics around post creation

- how long were people typing?
- how long was composer open?
- how many drafts were created?
- correct, draft saved to go away after you continue typing

store in Post.find(xyz).post_stat
This commit is contained in:
Sam 2015-08-03 14:29:04 +10:00
parent 5d40695908
commit 7b8b96446e
11 changed files with 148 additions and 12 deletions

View File

@ -413,7 +413,7 @@ export default Ember.ObjectController.extend(Presence, {
}
// we need a draft sequence for the composer to work
if (opts.draftSequence === void 0) {
if (opts.draftSequence === undefined) {
return Discourse.Draft.get(opts.draftKey).then(function(data) {
opts.draftSequence = data.draft_sequence;
opts.draft = data.draft;

View File

@ -22,7 +22,9 @@ const CLOSED = 'closed',
topic_id: 'topic.id',
is_warning: 'isWarning',
archetype: 'archetypeId',
target_usernames: 'targetUsernames'
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime'
},
_edit_topic_serializer = {
@ -52,6 +54,31 @@ const Composer = RestModel.extend({
viewOpen: Em.computed.equal('composeState', OPEN),
viewDraft: Em.computed.equal('composeState', DRAFT),
composeStateChanged: function() {
var oldOpen = this.get('composerOpened');
if (this.get('composeState') === OPEN) {
this.set('composerOpened', oldOpen || new Date());
} else {
if (oldOpen) {
var oldTotal = this.get('composerTotalOpened') || 0;
this.set('composerTotalOpened', oldTotal + (new Date() - oldOpen));
}
this.set('composerOpened', null);
}
}.observes('composeState'),
composerTime: function() {
var total = this.get('composerTotalOpened') || 0;
var oldOpen = this.get('composerOpened');
if (oldOpen) {
total += (new Date() - oldOpen);
}
return total;
}.property().volatile(),
archetype: function() {
return this.get('archetypes').findProperty('id', this.get('archetypeId'));
}.property('archetypeId'),
@ -60,6 +87,12 @@ const Composer = RestModel.extend({
return this.set('metaData', Em.Object.create());
}.observes('archetype'),
// view detected user is typing
typing: _.throttle(function(){
var typingTime = this.get("typingTime") || 0;
this.set("typingTime", typingTime + 100);
}, 100, {leading: false, trailing: true}),
editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'),
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
@ -349,7 +382,9 @@ const Composer = RestModel.extend({
composeState: opts.composerState || OPEN,
action: opts.action,
topic: opts.topic,
targetUsernames: opts.usernames
targetUsernames: opts.usernames,
composerTotalOpened: opts.composerTime,
typingTime: opts.typingTime
});
if (opts.post) {
@ -420,7 +455,10 @@ const Composer = RestModel.extend({
post: null,
title: null,
editReason: null,
stagedPost: false
stagedPost: false,
typingTime: 0,
composerOpened: null,
composerTotalOpened: 0
});
},
@ -502,7 +540,9 @@ const Composer = RestModel.extend({
admin: user.get('admin'),
yours: true,
read: true,
wiki: false
wiki: false,
typingTime: this.get('typingTime'),
composerTime: this.get('composerTime')
});
this.serialize(_create_serializer, createdPost);
@ -603,13 +643,20 @@ const Composer = RestModel.extend({
postId: this.get('post.id'),
archetypeId: this.get('archetypeId'),
metaData: this.get('metaData'),
usernames: this.get('targetUsernames')
usernames: this.get('targetUsernames'),
composerTime: this.get('composerTime'),
typingTime: this.get('typingTime')
};
this.set('draftStatus', I18n.t('composer.saving_draft_tip'));
const composer = this;
if (this._clearingStatus) {
Em.run.cancel(this._clearingStatus);
this._clearingStatus = null;
}
// try to save the draft
return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data)
.then(function() {
@ -617,7 +664,20 @@ const Composer = RestModel.extend({
}).catch(function() {
composer.set('draftStatus', I18n.t('composer.drafts_offline'));
});
}
},
dataChanged: function(){
const draftStatus = this.get('draftStatus');
const self = this;
if (draftStatus && !this._clearingStatus) {
this._clearingStatus = Em.run.later(this, function(){
self.set('draftStatus', null);
self._clearingStatus = null;
}, 1000);
}
}.observes('title','reply')
});
@ -657,7 +717,9 @@ Composer.reopenClass({
metaData: draft.metaData,
usernames: draft.usernames,
draft: true,
composerState: DRAFT
composerState: DRAFT,
composerTime: draft.composerTime,
typingTime: draft.typingTime
});
}
},

View File

@ -85,6 +85,8 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
const controller = this.get('controller');
controller.checkReplyLength();
this.get('controller.model').typing();
const lastKeyUp = new Date();
this.set('lastKeyUp', lastKeyUp);

View File

@ -403,7 +403,6 @@ class PostsController < ApplicationController
# Awful hack, but you can't seem to remove the `default_scope` when joining
# So instead I grab the topics separately
topic_ids = posts.dup.pluck(:topic_id)
secured_category_ids = guardian.secure_category_ids
topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message')
topics = topics.secured(guardian)
@ -422,7 +421,9 @@ class PostsController < ApplicationController
:category,
:target_usernames,
:reply_to_post_number,
:auto_track
:auto_track,
:typing_duration_msecs,
:composer_open_duration_msecs
]
# param munging for WordPress

View File

@ -7,7 +7,11 @@ class Draft < ActiveRecord::Base
d = find_draft(user,key)
if d
return if d.sequence > sequence
d.update_columns(data: data, sequence: sequence)
exec_sql("UPDATE drafts
SET data = :data,
sequence = :sequence,
revisions = revisions + 1
WHERE id = :id", id: d.id, sequence: sequence, data: data)
else
Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence)
end

View File

@ -37,6 +37,7 @@ class Post < ActiveRecord::Base
has_many :uploads, through: :post_uploads
has_one :post_search_data
has_one :post_stat
has_many :post_details

3
app/models/post_stat.rb Normal file
View File

@ -0,0 +1,3 @@
class PostStat < ActiveRecord::Base
belongs_to :post
end

View File

@ -0,0 +1,16 @@
class AddPostStats < ActiveRecord::Migration
def change
add_column :drafts, :revisions, :int, null: false, default: 1
create_table :post_stats do |t|
t.integer :post_id
t.integer :drafts_saved
t.integer :typing_duration_msecs
t.integer :composer_open_duration_msecs
t.timestamps
end
add_index :post_stats, [:post_id]
end
end

View File

@ -113,6 +113,7 @@ class PostCreator
def create
if valid?
transaction do
build_post_stats
create_topic
save_post
extract_links
@ -146,6 +147,14 @@ class PostCreator
@post
end
def self.track_post_stats
Rails.env != "test".freeze || @track_post_stats
end
def self.track_post_stats=(val)
@track_post_stats = val
end
def self.create(user, opts)
PostCreator.new(user, opts).create
end
@ -172,6 +181,23 @@ class PostCreator
protected
def build_post_stats
if PostCreator.track_post_stats
draft_key = @topic ? "topic_#{@topic.id}" : "new_topic"
sequence = DraftSequence.current(@user, draft_key)
revisions = Draft.where(sequence: sequence,
user_id: @user.id,
draft_key: draft_key).pluck(:revisions).first || 0
@post.build_post_stat(
drafts_saved: revisions,
typing_duration_msecs: @opts[:typing_duration_msecs] || 0,
composer_open_duration_msecs: @opts[:composer_open_duration_msecs] || 0
)
end
end
def trigger_after_events(post)
DiscourseEvent.trigger(:topic_created, post.topic, @opts, @user) unless @opts[:topic_id]
DiscourseEvent.trigger(:post_created, post, @opts, @user)

View File

@ -213,6 +213,21 @@ describe PostCreator do
}.to_not change { topic.excerpt }
end
it 'creates post stats' do
Draft.set(user, 'new_topic', 0, "test")
Draft.set(user, 'new_topic', 0, "test1")
begin
PostCreator.track_post_stats = true
post = creator.create
expect(post.post_stat.typing_duration_msecs).to eq(0)
expect(post.post_stat.drafts_saved).to eq(2)
ensure
PostCreator.track_post_stats = false
end
end
describe "topic's auto close" do
it "doesn't update topic's auto close when it's not based on last post" do

View File

@ -109,6 +109,12 @@ describe Draft do
expect(Draft.get(p.user, p.topic.draft_key, s)).to eq nil
end
it 'increases the sequence number when a post is revised'
it 'increases revision each time you set' do
u = User.first
Draft.set(u, 'new_topic', 0, 'hello')
Draft.set(u, 'new_topic', 0, 'goodbye')
expect(Draft.find_draft(u, 'new_topic').revisions).to eq(2)
end
end
end